/*-------------------------------------------------------------------------
 *
 * explain.c
 *	  解释查询执行计划
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994-5, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/backend/commands/explain.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/createas.h"
#include "commands/defrem.h"
#include "commands/prepare.h"
#include "executor/nodeHash.h"
#include "foreign/fdwapi.h"
#include "jit/jit.h"
#include "nodes/extensible.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "parser/analyze.h"
#include "parser/parsetree.h"
#include "rewrite/rewriteHandler.h"
#include "storage/bufmgr.h"
#include "tcop/tcopprot.h"
#include "utils/builtins.h"
#include "utils/guc_tables.h"
#include "utils/json.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
#include "utils/ruleutils.h"
#include "utils/snapmgr.h"
#include "utils/tuplesort.h"
#include "utils/typcache.h"
#include "utils/xml.h"


/* 钩子用于插件在 ExplainOneQuery() 中获取控制权 */
ExplainOneQuery_hook_type ExplainOneQuery_hook = NULL;

/* 钩子用于插件在 explain_get_index_name() 中获取控制权 */
explain_get_index_name_hook_type explain_get_index_name_hook = NULL;


/* 可 OR 连接的标志用于 ExplainXMLTag() */
#define X_OPENING 0
#define X_CLOSING 1
#define X_CLOSE_IMMEDIATE 2
#define X_NOWHITESPACE 4

static void fc_ExplainOneQuery(Query *fc_query, int fc_cursorOptions,
							IntoClause *fc_into, ExplainState *fc_es,
							const char *fc_queryString, ParamListInfo fc_params,
							QueryEnvironment *fc_queryEnv);
static void fc_ExplainPrintJIT(ExplainState *fc_es, int fc_jit_flags,
							JitInstrumentation *fc_ji);
static void fc_report_triggers(ResultRelInfo *fc_rInfo, bool fc_show_relname,
							ExplainState *fc_es);
static double fc_elapsed_time(instr_time *fc_starttime);
static bool fc_ExplainPreScanNode(PlanState *fc_planstate, Bitmapset **fc_rels_used);
static void fc_ExplainNode(PlanState *fc_planstate, List *fc_ancestors,
						const char *fc_relationship, const char *fc_plan_name,
						ExplainState *fc_es);
static void fc_show_plan_tlist(PlanState *fc_planstate, List *fc_ancestors,
							ExplainState *fc_es);
static void fc_show_expression(Node *fc_node, const char *fc_qlabel,
							PlanState *fc_planstate, List *fc_ancestors,
							bool fc_useprefix, ExplainState *fc_es);
static void fc_show_qual(List *fc_qual, const char *fc_qlabel,
					  PlanState *fc_planstate, List *fc_ancestors,
					  bool fc_useprefix, ExplainState *fc_es);
static void fc_show_scan_qual(List *fc_qual, const char *fc_qlabel,
						   PlanState *fc_planstate, List *fc_ancestors,
						   ExplainState *fc_es);
static void fc_show_upper_qual(List *fc_qual, const char *fc_qlabel,
							PlanState *fc_planstate, List *fc_ancestors,
							ExplainState *fc_es);
static void fc_show_sort_keys(SortState *fc_sortstate, List *fc_ancestors,
						   ExplainState *fc_es);
static void fc_show_incremental_sort_keys(IncrementalSortState *fc_incrsortstate,
									   List *fc_ancestors, ExplainState *fc_es);
static void fc_show_merge_append_keys(MergeAppendState *fc_mstate, List *fc_ancestors,
								   ExplainState *fc_es);
static void fc_show_agg_keys(AggState *fc_astate, List *fc_ancestors,
						  ExplainState *fc_es);
static void fc_show_grouping_sets(PlanState *fc_planstate, Agg *fc_agg,
							   List *fc_ancestors, ExplainState *fc_es);
static void fc_show_grouping_set_keys(PlanState *fc_planstate,
								   Agg *fc_aggnode, Sort *fc_sortnode,
								   List *fc_context, bool fc_useprefix,
								   List *fc_ancestors, ExplainState *fc_es);
static void fc_show_group_keys(GroupState *fc_gstate, List *fc_ancestors,
							ExplainState *fc_es);
static void fc_show_sort_group_keys(PlanState *fc_planstate, const char *fc_qlabel,
								 int fc_nkeys, int fc_nPresortedKeys, AttrNumber *fc_keycols,
								 Oid *fc_sortOperators, Oid *fc_collations, bool *fc_nullsFirst,
								 List *fc_ancestors, ExplainState *fc_es);
static void fc_show_sortorder_options(StringInfo fc_buf, Node *fc_sortexpr,
								   Oid fc_sortOperator, Oid fc_collation, bool fc_nullsFirst);
static void fc_show_tablesample(TableSampleClause *fc_tsc, PlanState *fc_planstate,
							 List *fc_ancestors, ExplainState *fc_es);
static void fc_show_sort_info(SortState *fc_sortstate, ExplainState *fc_es);
static void fc_show_incremental_sort_info(IncrementalSortState *fc_incrsortstate,
									   ExplainState *fc_es);
static void fc_show_hash_info(HashState *fc_hashstate, ExplainState *fc_es);
static void fc_show_memoize_info(MemoizeState *fc_mstate, List *fc_ancestors,
							  ExplainState *fc_es);
static void fc_show_hashagg_info(AggState *fc_hashstate, ExplainState *fc_es);
static void fc_show_tidbitmap_info(BitmapHeapScanState *fc_planstate,
								ExplainState *fc_es);
static void fc_show_instrumentation_count(const char *fc_qlabel, int fc_which,
									   PlanState *fc_planstate, ExplainState *fc_es);
static void fc_show_foreignscan_info(ForeignScanState *fc_fsstate, ExplainState *fc_es);
static void fc_show_eval_params(Bitmapset *fc_bms_params, ExplainState *fc_es);
static const char *fc_explain_get_index_name(Oid fc_indexId);
static void fc_show_buffer_usage(ExplainState *fc_es, const BufferUsage *fc_usage,
							  bool fc_planning);
static void fc_show_wal_usage(ExplainState *fc_es, const WalUsage *fc_usage);
static void fc_ExplainIndexScanDetails(Oid fc_indexid, ScanDirection fc_indexorderdir,
									ExplainState *fc_es);
static void fc_ExplainScanTarget(Scan *fc_plan, ExplainState *fc_es);
static void fc_ExplainModifyTarget(ModifyTable *fc_plan, ExplainState *fc_es);
static void fc_ExplainTargetRel(Plan *fc_plan, Index fc_rti, ExplainState *fc_es);
static void fc_show_modifytable_info(ModifyTableState *fc_mtstate, List *fc_ancestors,
								  ExplainState *fc_es);
static void fc_ExplainMemberNodes(PlanState **fc_planstates, int fc_nplans,
							   List *fc_ancestors, ExplainState *fc_es);
static void fc_ExplainMissingMembers(int fc_nplans, int fc_nchildren, ExplainState *fc_es);
static void fc_ExplainSubPlans(List *fc_plans, List *fc_ancestors,
							const char *fc_relationship, ExplainState *fc_es);
static void fc_ExplainCustomChildren(CustomScanState *fc_css,
								  List *fc_ancestors, ExplainState *fc_es);
static ExplainWorkersState *fc_ExplainCreateWorkersState(int fc_num_workers);
static void fc_ExplainOpenWorker(int fc_n, ExplainState *fc_es);
static void fc_ExplainCloseWorker(int fc_n, ExplainState *fc_es);
static void fc_ExplainFlushWorkersState(ExplainState *fc_es);
static void fc_ExplainProperty(const char *fc_qlabel, const char *fc_unit,
							const char *fc_value, bool fc_numeric, ExplainState *fc_es);
static void fc_ExplainOpenSetAsideGroup(const char *fc_objtype, const char *fc_labelname,
									 bool fc_labeled, int fc_depth, ExplainState *fc_es);
static void fc_ExplainSaveGroup(ExplainState *fc_es, int fc_depth, int *fc_state_save);
static void fc_ExplainRestoreGroup(ExplainState *fc_es, int fc_depth, int *fc_state_save);
static void fc_ExplainDummyGroup(const char *fc_objtype, const char *fc_labelname,
							  ExplainState *fc_es);
static void fc_ExplainXMLTag(const char *fc_tagname, int fc_flags, ExplainState *fc_es);
static void fc_ExplainIndentText(ExplainState *fc_es);
static void fc_ExplainJSONLineEnding(ExplainState *fc_es);
static void fc_ExplainYAMLLineStarting(ExplainState *fc_es);
static void fc_escape_yaml(StringInfo fc_buf, const char *fc_str);



/*
 * ExplainQuery -
 *	  执行 EXPLAIN 命令
 */
void ExplainQuery(ParseState *fc_pstate, ExplainStmt *fc_stmt,
			 ParamListInfo fc_params, DestReceiver *fc_dest)
{
	ExplainState *fc_es = NewExplainState();
	TupOutputState *fc_tstate;
	JumbleState *fc_jstate = NULL;
	Query	   *fc_query;
	List	   *fc_rewritten;
	ListCell   *fc_lc;
	bool		fc_timing_set = false;
	bool		fc_summary_set = false;

	/* 解析选项列表。 */
	foreach(fc_lc, fc_stmt->options)
	{
		DefElem    *fc_opt = (DefElem *) lfirst(fc_lc);

		if (strcmp(fc_opt->defname, "analyze") == 0)
			fc_es->analyze = defGetBoolean(fc_opt);
		else if (strcmp(fc_opt->defname, "verbose") == 0)
			fc_es->verbose = defGetBoolean(fc_opt);
		else if (strcmp(fc_opt->defname, "costs") == 0)
			fc_es->costs = defGetBoolean(fc_opt);
		else if (strcmp(fc_opt->defname, "buffers") == 0)
			fc_es->buffers = defGetBoolean(fc_opt);
		else if (strcmp(fc_opt->defname, "wal") == 0)
			fc_es->wal = defGetBoolean(fc_opt);
		else if (strcmp(fc_opt->defname, "settings") == 0)
			fc_es->settings = defGetBoolean(fc_opt);
		else if (strcmp(fc_opt->defname, "timing") == 0)
		{
			fc_timing_set = true;
			fc_es->timing = defGetBoolean(fc_opt);
		}
		else if (strcmp(fc_opt->defname, "summary") == 0)
		{
			fc_summary_set = true;
			fc_es->summary = defGetBoolean(fc_opt);
		}
		else if (strcmp(fc_opt->defname, "format") == 0)
		{
			char	   *fc_p = defGetString(fc_opt);

			if (strcmp(fc_p, "text") == 0)
				fc_es->format = EXPLAIN_FORMAT_TEXT;
			else if (strcmp(fc_p, "xml") == 0)
				fc_es->format = EXPLAIN_FORMAT_XML;
			else if (strcmp(fc_p, "json") == 0)
				fc_es->format = EXPLAIN_FORMAT_JSON;
			else if (strcmp(fc_p, "yaml") == 0)
				fc_es->format = EXPLAIN_FORMAT_YAML;
			else
				ereport(ERROR,
						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
						 errmsg("unrecognized value for EXPLAIN option \"%s\": \"%s\"",
								fc_opt->defname, fc_p),
						 parser_errposition(fc_pstate, fc_opt->location)));
		}
		else
			ereport(ERROR,
					(errcode(ERRCODE_SYNTAX_ERROR),
					 errmsg("unrecognized EXPLAIN option \"%s\"",
							fc_opt->defname),
					 parser_errposition(fc_pstate, fc_opt->location)));
	}

	if (fc_es->wal && !fc_es->analyze)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("EXPLAIN option WAL requires ANALYZE")));

	/* 如果计时未明确设置，则设置默认值 */
	fc_es->timing = (fc_timing_set) ? fc_es->timing : fc_es->analyze;

	/* 检查计时是否与 EXPLAIN ANALYZE 一起使用 */
	if (fc_es->timing && !fc_es->analyze)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("EXPLAIN option TIMING requires ANALYZE")));

	/* 如果摘要未明确设置，则设置默认值 */
	fc_es->summary = (fc_summary_set) ? fc_es->summary : fc_es->analyze;

	fc_query = castNode(Query, fc_stmt->query);
	if (IsQueryIdEnabled())
		fc_jstate = JumbleQuery(fc_query, fc_pstate->p_sourcetext);

	if (post_parse_analyze_hook)
		(*post_parse_analyze_hook) (fc_pstate, fc_query, fc_jstate);

	/*
	 * 解析分析已经完成，但我们仍然需要运行规则
	 * 重写器。我们不执行 AcquireRewriteLocks：我们假设查询要么
	 * 直接来自解析器，或者适当的锁由
	 * plancache.c 获取。
	 */
	fc_rewritten = QueryRewrite(castNode(Query, fc_stmt->query));

	/* 发出开头的模板 */
	ExplainBeginOutput(fc_es);

	if (fc_rewritten == NIL)
	{
		/*
		 * 在 INSTEAD NOTHING 的情况下，至少告诉这一点。但是在
		 * 非文本格式下，输出是分隔的，所以这并不是必要的。
		 */
		if (fc_es->format == EXPLAIN_FORMAT_TEXT)
			appendStringInfoString(fc_es->str, "Query rewrites to nothing\n");
	}
	else
	{
		ListCell   *fc_l;

		/* 解释每个计划 */
		foreach(fc_l, fc_rewritten)
		{
			fc_ExplainOneQuery(lfirst_node(Query, fc_l),
							CURSOR_OPT_PARALLEL_OK, NULL, fc_es,
							fc_pstate->p_sourcetext, fc_params, fc_pstate->p_queryEnv);

			/* 用适当的分隔符分隔计划 */
			if (lnext(fc_rewritten, fc_l) != NULL)
				ExplainSeparatePlans(fc_es);
		}
	}

	/* 发出结尾的模板 */
	ExplainEndOutput(fc_es);
	Assert(fc_es->indent == 0);

	/* 输出元组 */
	fc_tstate = begin_tup_output_tupdesc(fc_dest, ExplainResultDesc(fc_stmt),
									  &TTSOpsVirtual);
	if (fc_es->format == EXPLAIN_FORMAT_TEXT)
		do_text_output_multiline(fc_tstate, fc_es->str->data);
	else
		do_text_output_oneline(fc_tstate, fc_es->str->data);
	end_tup_output(fc_tstate);

	pfree(fc_es->str->data);
}

/*
 * Create a new ExplainState struct initialized with default options.
 */
ExplainState *
NewExplainState(void)
{
	ExplainState *fc_es = (ExplainState *) palloc0(sizeof(ExplainState));

	/* 设置默认选项（大多数字段可以保持为零）。 */
	fc_es->costs = true;
	/* 准备输出缓冲区。 */
	fc_es->str = makeStringInfo();

	return fc_es;
}

/*
 * ExplainResultDesc -
 *	  构造 EXPLAIN 的结果 tupledesc
 */
TupleDesc ExplainResultDesc(ExplainStmt *fc_stmt)
{
	TupleDesc	fc_tupdesc;
	ListCell   *fc_lc;
	Oid			fc_result_type = TEXTOID;

	/* 检查 XML 格式选项 */
	foreach(fc_lc, fc_stmt->options)
	{
		DefElem    *fc_opt = (DefElem *) lfirst(fc_lc);

		if (strcmp(fc_opt->defname, "format") == 0)
		{
			char	   *fc_p = defGetString(fc_opt);

			if (strcmp(fc_p, "xml") == 0)
				fc_result_type = XMLOID;
			else if (strcmp(fc_p, "json") == 0)
				fc_result_type = JSONOID;
			else
				fc_result_type = TEXTOID;
			/* 不要“中断”，因为 ExplainQuery 将使用最后一个值 */
		}
	}

	/* 需要一个表示单个 TEXT 或 XML 列的元组描述符 */
	fc_tupdesc = CreateTemplateTupleDesc(1);
	TupleDescInitEntry(fc_tupdesc, (AttrNumber) 1, "QUERY PLAN",
					   fc_result_type, -1, 0);
	return fc_tupdesc;
}

/*
 * ExplainOneQuery -
 *	  打印单个查询的执行计划
 *
 * "into" 为 NULL，除非我们在解释 CreateTableAsStmt 的内容。
 */
static void fc_ExplainOneQuery(Query *fc_query, int fc_cursorOptions,
				IntoClause *fc_into, ExplainState *fc_es,
				const char *fc_queryString, ParamListInfo fc_params,
				QueryEnvironment *fc_queryEnv)
{
	/* 计划器无法处理实用程序语句 */
	if (fc_query->commandType == CMD_UTILITY)
	{
		ExplainOneUtility(fc_query->utilityStmt, fc_into, fc_es, fc_queryString, fc_params,
						  fc_queryEnv);
		return;
	}

	/* 如果有顾问插件，允许其管理事务 */
	if (ExplainOneQuery_hook)
		(*ExplainOneQuery_hook) (fc_query, fc_cursorOptions, fc_into, fc_es,
								 fc_queryString, fc_params, fc_queryEnv);
	else
	{
		PlannedStmt *fc_plan;
		instr_time	fc_planstart,
					fc_planduration;
		BufferUsage fc_bufusage_start,
					fc_bufusage;

		if (fc_es->buffers)
			fc_bufusage_start = pgBufferUsage;
		INSTR_TIME_SET_CURRENT(fc_planstart);

		/* 计划查询 */
		fc_plan = pg_plan_query(fc_query, fc_queryString, fc_cursorOptions, fc_params);

		INSTR_TIME_SET_CURRENT(fc_planduration);
		INSTR_TIME_SUBTRACT(fc_planduration, fc_planstart);

		/* 计算缓冲计数器的差异。 */
		if (fc_es->buffers)
		{
			memset(&fc_bufusage, 0, sizeof(BufferUsage));
			BufferUsageAccumDiff(&fc_bufusage, &pgBufferUsage, &fc_bufusage_start);
		}

		/* 运行它（如果需要）并产生输出 */
		ExplainOnePlan(fc_plan, fc_into, fc_es, fc_queryString, fc_params, fc_queryEnv,
					   &fc_planduration, (fc_es->buffers ? &fc_bufusage : NULL));
	}
}

/*
 * ExplainOneUtility -
 *	  打印单个实用程序语句的执行计划
 *	  （一般来说，实用程序语句没有计划，但有些
 *	  我们视为特例）
 *
 * "into" 为 NULL，除非我们在解释 CreateTableAsStmt 的内容。
 *
 * 这被导出，因为它是从 prepare.c 中调用的
 * EXPLAIN EXECUTE 情况。在这种情况下，我们将处理一个
 * 在计划缓存中的语句，因此必须确保我们不修改它。
 */
void ExplainOneUtility(Node *fc_utilityStmt, IntoClause *fc_into, ExplainState *fc_es,
				  const char *fc_queryString, ParamListInfo fc_params,
				  QueryEnvironment *fc_queryEnv)
{
	if (fc_utilityStmt == NULL)
		return;

	if (IsA(fc_utilityStmt, CreateTableAsStmt))
	{
		/*
		 * 我们必须重写包含的 SELECT，然后将其传回
		 * ExplainOneQuery。复制是为了在 EXPLAIN EXECUTE 情况下安全。
		 */
		CreateTableAsStmt *fc_ctas = (CreateTableAsStmt *) fc_utilityStmt;
		List	   *fc_rewritten;

		/*
		 * 检查关系是否存在。这在此阶段进行，以
		 * 避免查询规划或执行。
		 */
		if (CreateTableAsRelExists(fc_ctas))
		{
			if (fc_ctas->objtype == OBJECT_TABLE)
				fc_ExplainDummyGroup("CREATE TABLE AS", NULL, fc_es);
			else if (fc_ctas->objtype == OBJECT_MATVIEW)
				fc_ExplainDummyGroup("CREATE MATERIALIZED VIEW", NULL, fc_es);
			else
				elog(ERROR, "unexpected object type: %d",
					 (int) fc_ctas->objtype);
			return;
		}

		fc_rewritten = QueryRewrite(castNode(Query, copyObject(fc_ctas->query)));
		Assert(list_length(fc_rewritten) == 1);
		fc_ExplainOneQuery(linitial_node(Query, fc_rewritten),
						CURSOR_OPT_PARALLEL_OK, fc_ctas->into, fc_es,
						fc_queryString, fc_params, fc_queryEnv);
	}
	else if (IsA(fc_utilityStmt, DeclareCursorStmt))
	{
		/*
		 * DECLARE CURSOR 同样适用。
		 *
		 * 注意，如果你说 EXPLAIN ANALYZE DECLARE CURSOR，那么我们将
		 * 实际运行查询。这与 8.3 之前的行为不同
		 * 但似乎比不运行查询更有用。然而不会
		 * 创建游标。
		 */
		DeclareCursorStmt *fc_dcs = (DeclareCursorStmt *) fc_utilityStmt;
		List	   *fc_rewritten;

		fc_rewritten = QueryRewrite(castNode(Query, copyObject(fc_dcs->query)));
		Assert(list_length(fc_rewritten) == 1);
		fc_ExplainOneQuery(linitial_node(Query, fc_rewritten),
						fc_dcs->options, NULL, fc_es,
						fc_queryString, fc_params, fc_queryEnv);
	}
	else if (IsA(fc_utilityStmt, ExecuteStmt))
		ExplainExecuteQuery((ExecuteStmt *) fc_utilityStmt, fc_into, fc_es,
							fc_queryString, fc_params, fc_queryEnv);
	else if (IsA(fc_utilityStmt, NotifyStmt))
	{
		if (fc_es->format == EXPLAIN_FORMAT_TEXT)
			appendStringInfoString(fc_es->str, "NOTIFY\n");
		else
			fc_ExplainDummyGroup("Notify", NULL, fc_es);
	}
	else
	{
		if (fc_es->format == EXPLAIN_FORMAT_TEXT)
			appendStringInfoString(fc_es->str,
								   "Utility statements have no plan structure\n");
		else
			fc_ExplainDummyGroup("Utility Statement", NULL, fc_es);
	}
}

/*
 * ExplainOnePlan -
 *		给定一个计划查询，如果需要则执行该查询，然后打印
 *		EXPLAIN 输出
 *
 * "into" 为 NULL，除非我们正在解释 CreateTableAsStmt 的内容，
 * 在这种情况下执行查询应会导致创建该表。
 *
 * 这是导出的，因为它是在 EXPLAIN EXECUTE 情况下从 prepare.c 回调的，
 * 并且因为索引顾问插件需要调用它。
 */
void ExplainOnePlan(PlannedStmt *fc_plannedstmt, IntoClause *fc_into, ExplainState *fc_es,
			   const char *fc_queryString, ParamListInfo fc_params,
			   QueryEnvironment *fc_queryEnv, const instr_time *fc_planduration,
			   const BufferUsage *fc_bufusage)
{
	DestReceiver *fc_dest;
	QueryDesc  *fc_queryDesc;
	instr_time	fc_starttime;
	double		fc_totaltime = 0;
	int			fc_eflags;
	int			fc_instrument_option = 0;

	Assert(fc_plannedstmt->commandType != CMD_UTILITY);

	if (fc_es->analyze && fc_es->timing)
		fc_instrument_option |= INSTRUMENT_TIMER;
	else if (fc_es->analyze)
		fc_instrument_option |= INSTRUMENT_ROWS;

	if (fc_es->buffers)
		fc_instrument_option |= INSTRUMENT_BUFFERS;
	if (fc_es->wal)
		fc_instrument_option |= INSTRUMENT_WAL;

	/*
	 * 我们始终收集整个语句的计时，即使在节点级别
	 * 计时关闭时，所以我们在这里不查看 es->timing。 (如果 !es->summary,
	 * 我们可以跳过这一步，但这几乎没必要复杂化。)
	 */
	INSTR_TIME_SET_CURRENT(fc_starttime);

	/*
	 * 使用带有更新命令 ID 的快照以确保该查询看到
	 * 任何先前执行查询的结果。
	 */
	PushCopiedSnapshot(GetActiveSnapshot());
	UpdateActiveSnapshotCommandId();

	/*
	 * 通常我们会丢弃查询的输出，但如果在解释 CREATE TABLE
	 * AS，我们最好使用合适的元组接收器。
	 */
	if (fc_into)
		fc_dest = CreateIntoRelDestReceiver(fc_into);
	else
		fc_dest = None_Receiver;

	/* 为查询创建一个 QueryDesc */
	fc_queryDesc = CreateQueryDesc(fc_plannedstmt, fc_queryString,
								GetActiveSnapshot(), InvalidSnapshot,
								fc_dest, fc_params, fc_queryEnv, fc_instrument_option);

	/* 选择执行选项 */
	if (fc_es->analyze)
		fc_eflags = 0;				/* 默认完成标志 */
	else
		fc_eflags = EXEC_FLAG_EXPLAIN_ONLY;
	if (fc_into)
		fc_eflags |= GetIntoRelEFlags(fc_into);

	/* 调用 ExecutorStart 准备执行计划 */
	ExecutorStart(fc_queryDesc, fc_eflags);

	/* 如果请求统计信息，则执行该计划 */
	if (fc_es->analyze)
	{
		ScanDirection fc_dir;

		/* EXPLAIN ANALYZE CREATE TABLE AS WITH NO DATA 是奇怪的 */
		if (fc_into && fc_into->skipData)
			fc_dir = NoMovementScanDirection;
		else
			fc_dir = ForwardScanDirection;

		/* 运行计划 */
		ExecutorRun(fc_queryDesc, fc_dir, 0L, true);

		/* 也要运行清理 */
		ExecutorFinish(fc_queryDesc);

		/* 在我们完成打印统计信息之前，不能运行 ExecutorEnd... */
		fc_totaltime += fc_elapsed_time(&fc_starttime);
	}

	ExplainOpenGroup("Query", NULL, true, fc_es);

	/* 创建计划树的文本转储 */
	ExplainPrintPlan(fc_es, fc_queryDesc);

	/*
	 * COMPUTE_QUERY_ID_REGRESS 意味着 COMPUTE_QUERY_ID_AUTO，
	 * 但我们不在任何 EXPLAIN 计划中显示 queryid，以保持
	 * 回归测试套件生成的结果稳定。
	 */
	if (fc_es->verbose && fc_plannedstmt->queryId != UINT64CONST(0) &&
		compute_query_id != COMPUTE_QUERY_ID_REGRESS)
	{
		/*
		 * 将 queryid 作为 int64 输出，而不是 uint64，以便与
		 * 在 BIGINT pg_stat_statements.queryid 列中看到的相匹配。
		 */
		ExplainPropertyInteger("Query Identifier", NULL, (int64)
							   fc_plannedstmt->queryId, fc_es);
	}

	/* 显示计划中的缓冲区使用情况 */
	if (fc_bufusage)
	{
		ExplainOpenGroup("Planning", "Planning", true, fc_es);
		fc_show_buffer_usage(fc_es, fc_bufusage, true);
		ExplainCloseGroup("Planning", "Planning", true, fc_es);
	}

	if (fc_es->summary && fc_planduration)
	{
		double		fc_plantime = INSTR_TIME_GET_DOUBLE(*fc_planduration);

		ExplainPropertyFloat("Planning Time", "ms", 1000.0 * fc_plantime, 3, fc_es);
	}

	/* 打印触发器运行时的信息 */
	if (fc_es->analyze)
		ExplainPrintTriggers(fc_es, fc_queryDesc);

	/*
	 * 打印关于 JIT 的信息。与 es->costs 相关，因为我们不想在
	 * 回归测试中显示这些，因为这会导致输出差异
	 * 取决于构建选项。可能需要在以后将其与 COSTS 分开。
	 */
	if (fc_es->costs)
		ExplainPrintJITSummary(fc_es, fc_queryDesc);

	/*
	 * 关闭查询并释放资源。在总执行时间中包括此时间
	 * （尽管它应该非常最小）。
	 */
	INSTR_TIME_SET_CURRENT(fc_starttime);

	ExecutorEnd(fc_queryDesc);

	FreeQueryDesc(fc_queryDesc);

	PopActiveSnapshot();

	/* 我们需要一个 CCI，以防查询扩展到多个计划 */
	if (fc_es->analyze)
		CommandCounterIncrement();

	fc_totaltime += fc_elapsed_time(&fc_starttime);

	/*
	 * 我们只报告执行时间，如果我们实际运行了查询（即，
	 * 用户指定了 ANALYZE），并且如果启用了汇总报告（用户
	 * 可以设置 SUMMARY OFF 以不将计时信息包含在输出中）。 
	 * 默认情况下，ANALYZE 将 SUMMARY 设置为 true。
	 */
	if (fc_es->summary && fc_es->analyze)
		ExplainPropertyFloat("Execution Time", "ms", 1000.0 * fc_totaltime, 3,
							 fc_es);

	ExplainCloseGroup("Query", NULL, true, fc_es);
}

/*
 * ExplainPrintSettings -
 *    打印影响查询规划的修改设置的摘要。
 */
static void fc_ExplainPrintSettings(ExplainState *fc_es)
{
	int			fc_num;
	struct config_generic **fc_gucs;

	/* 如果未请求设置的信息，则退出 */
	if (!fc_es->settings)
		return;

	/* 请求相关设置的数组 */
	fc_gucs = get_explain_guc_options(&fc_num);

	if (fc_es->format != EXPLAIN_FORMAT_TEXT)
	{
		ExplainOpenGroup("Settings", "Settings", true, fc_es);

		for (int fc_i = 0; fc_i < fc_num; fc_i++)
		{
			char	   *fc_setting;
			struct config_generic *fc_conf = fc_gucs[fc_i];

			fc_setting = GetConfigOptionByName(fc_conf->name, NULL, true);

			ExplainPropertyText(fc_conf->name, fc_setting, fc_es);
		}

		ExplainCloseGroup("Settings", "Settings", true, fc_es);
	}
	else
	{
		StringInfoData fc_str;

		/* 在 TEXT 模式下，如果没有选项则不打印任何内容 */
		if (fc_num <= 0)
			return;

		initStringInfo(&fc_str);

		for (int fc_i = 0; fc_i < fc_num; fc_i++)
		{
			char	   *fc_setting;
			struct config_generic *fc_conf = fc_gucs[fc_i];

			if (fc_i > 0)
				appendStringInfoString(&fc_str, ", ");

			fc_setting = GetConfigOptionByName(fc_conf->name, NULL, true);

			if (fc_setting)
				appendStringInfo(&fc_str, "%s = '%s'", fc_conf->name, fc_setting);
			else
				appendStringInfo(&fc_str, "%s = NULL", fc_conf->name);
		}

		ExplainPropertyText("Settings", fc_str.data, fc_es);
	}
}

/*
 * ExplainPrintPlan -
 *	  将 QueryDesc 的计划树转换为文本，并附加到 es->str
 *
 * 调用者应已设置 *es 的选项字段，并初始化输出缓冲区 es->str。同时，输出格式状态，如缩进级别被假定为有效。*es 中与计划树特定的字段在此初始化。
 *
 * 注意：在实用语句上无效
 */
void ExplainPrintPlan(ExplainState *fc_es, QueryDesc *fc_queryDesc)
{
	Bitmapset  *fc_rels_used = NULL;
	PlanState  *fc_ps;

	/* 设置与此计划树关联的 ExplainState 字段 */
	Assert(fc_queryDesc->plannedstmt != NULL);
	fc_es->pstmt = fc_queryDesc->plannedstmt;
	fc_es->rtable = fc_queryDesc->plannedstmt->rtable;
	fc_ExplainPreScanNode(fc_queryDesc->planstate, &fc_rels_used);
	fc_es->rtable_names = select_rtable_names_for_explain(fc_es->rtable, fc_rels_used);
	fc_es->deparse_cxt = deparse_context_for_plan_tree(fc_queryDesc->plannedstmt,
													fc_es->rtable_names);
	fc_es->printed_subplans = NULL;

	/*
	 * 有时我们将 Gather 节点标记为“不可见”，这意味着
	 * 不会在 EXPLAIN 输出中显示。这样做的目的是为了允许
	 * 以 force_parallel_mode=regress 运行回归测试，得到
	 * 与以 force_parallel_mode=off 运行相同测试相同的结果。
	 * 这种标记目前仅支持计划顶部的 Gather。我们跳过该节点，
	 * 同时还必须在计划树的更深处隐藏每个工作线程的详细数据。
	 */
	fc_ps = fc_queryDesc->planstate;
	if (IsA(fc_ps, GatherState) && ((Gather *) fc_ps->plan)->invisible)
	{
		fc_ps = outerPlanState(fc_ps);
		fc_es->hide_workers = true;
	}
	fc_ExplainNode(fc_ps, NIL, NULL, NULL, fc_es);

	/*
	 * 如果请求，包含与内置默认值不匹配的 GUC 参数的值的信息。
	 */
	fc_ExplainPrintSettings(fc_es);
}

/*
 * ExplainPrintTriggers -
 *	  将 QueryDesc 的触发器统计信息转换为文本并附加到
 *	  es->str
 *
 * 调用者应已设置 *es 的选项字段，并初始化输出缓冲区 es->str。*es 中的其他字段在此初始化。
 */
void ExplainPrintTriggers(ExplainState *fc_es, QueryDesc *fc_queryDesc)
{
	ResultRelInfo *fc_rInfo;
	bool		fc_show_relname;
	List	   *fc_resultrels;
	List	   *fc_routerels;
	List	   *fc_targrels;
	ListCell   *fc_l;

	fc_resultrels = fc_queryDesc->estate->es_opened_result_relations;
	fc_routerels = fc_queryDesc->estate->es_tuple_routing_result_relations;
	fc_targrels = fc_queryDesc->estate->es_trig_target_relations;

	ExplainOpenGroup("Triggers", "Triggers", false, fc_es);

	fc_show_relname = (list_length(fc_resultrels) > 1 ||
					fc_routerels != NIL || fc_targrels != NIL);
	foreach(fc_l, fc_resultrels)
	{
		fc_rInfo = (ResultRelInfo *) lfirst(fc_l);
		fc_report_triggers(fc_rInfo, fc_show_relname, fc_es);
	}

	foreach(fc_l, fc_routerels)
	{
		fc_rInfo = (ResultRelInfo *) lfirst(fc_l);
		fc_report_triggers(fc_rInfo, fc_show_relname, fc_es);
	}

	foreach(fc_l, fc_targrels)
	{
		fc_rInfo = (ResultRelInfo *) lfirst(fc_l);
		fc_report_triggers(fc_rInfo, fc_show_relname, fc_es);
	}

	ExplainCloseGroup("Triggers", "Triggers", false, fc_es);
}

/*
 * ExplainPrintJITSummary -
 *    打印来自领导者和工作线程的 JIT 工具的总结
 */
void ExplainPrintJITSummary(ExplainState *fc_es, QueryDesc *fc_queryDesc)
{
	JitInstrumentation fc_ji = {0};

	if (!(fc_queryDesc->estate->es_jit_flags & PGJIT_PERFORM))
		return;

	/*
	 * 使用副本而不是修改领导者状态，因为此
	 * 函数可能会被调用两次
	 */
	if (fc_queryDesc->estate->es_jit)
		InstrJitAgg(&fc_ji, &fc_queryDesc->estate->es_jit->instr);

	/* 如果此进程已在并行工作线程中进行 JIT，则合并统计数据 */
	if (fc_queryDesc->estate->es_jit_worker_instr)
		InstrJitAgg(&fc_ji, fc_queryDesc->estate->es_jit_worker_instr);

	fc_ExplainPrintJIT(fc_es, fc_queryDesc->estate->es_jit_flags, &fc_ji);
}

/*
 * ExplainPrintJIT -
 *	  将有关 JIT 的信息附加到 es->str。
 */
static void fc_ExplainPrintJIT(ExplainState *fc_es, int fc_jit_flags, JitInstrumentation *fc_ji)
{
	instr_time	fc_total_time;

	/* 如果没有发生 JIT，则不打印信息 */
	if (!fc_ji || fc_ji->created_functions == 0)
		return;

	/* 计算总时间 */
	INSTR_TIME_SET_ZERO(fc_total_time);
	INSTR_TIME_ADD(fc_total_time, fc_ji->generation_counter);
	INSTR_TIME_ADD(fc_total_time, fc_ji->inlining_counter);
	INSTR_TIME_ADD(fc_total_time, fc_ji->optimization_counter);
	INSTR_TIME_ADD(fc_total_time, fc_ji->emission_counter);

	ExplainOpenGroup("JIT", "JIT", true, fc_es);

	/* 为了更高的密度，开放代码文本输出格式 */
	if (fc_es->format == EXPLAIN_FORMAT_TEXT)
	{
		fc_ExplainIndentText(fc_es);
		appendStringInfoString(fc_es->str, "JIT:\n");
		fc_es->indent++;

		ExplainPropertyInteger("Functions", NULL, fc_ji->created_functions, fc_es);

		fc_ExplainIndentText(fc_es);
		appendStringInfo(fc_es->str, "Options: %s %s, %s %s, %s %s, %s %s\n",
						 "Inlining", fc_jit_flags & PGJIT_INLINE ? "true" : "false",
						 "Optimization", fc_jit_flags & PGJIT_OPT3 ? "true" : "false",
						 "Expressions", fc_jit_flags & PGJIT_EXPR ? "true" : "false",
						 "Deforming", fc_jit_flags & PGJIT_DEFORM ? "true" : "false");

		if (fc_es->analyze && fc_es->timing)
		{
			fc_ExplainIndentText(fc_es);
			appendStringInfo(fc_es->str,
							 "Timing: %s %.3f ms, %s %.3f ms, %s %.3f ms, %s %.3f ms, %s %.3f ms\n",
							 "Generation", 1000.0 * INSTR_TIME_GET_DOUBLE(fc_ji->generation_counter),
							 "Inlining", 1000.0 * INSTR_TIME_GET_DOUBLE(fc_ji->inlining_counter),
							 "Optimization", 1000.0 * INSTR_TIME_GET_DOUBLE(fc_ji->optimization_counter),
							 "Emission", 1000.0 * INSTR_TIME_GET_DOUBLE(fc_ji->emission_counter),
							 "Total", 1000.0 * INSTR_TIME_GET_DOUBLE(fc_total_time));
		}

		fc_es->indent--;
	}
	else
	{
		ExplainPropertyInteger("Functions", NULL, fc_ji->created_functions, fc_es);

		ExplainOpenGroup("Options", "Options", true, fc_es);
		ExplainPropertyBool("Inlining", fc_jit_flags & PGJIT_INLINE, fc_es);
		ExplainPropertyBool("Optimization", fc_jit_flags & PGJIT_OPT3, fc_es);
		ExplainPropertyBool("Expressions", fc_jit_flags & PGJIT_EXPR, fc_es);
		ExplainPropertyBool("Deforming", fc_jit_flags & PGJIT_DEFORM, fc_es);
		ExplainCloseGroup("Options", "Options", true, fc_es);

		if (fc_es->analyze && fc_es->timing)
		{
			ExplainOpenGroup("Timing", "Timing", true, fc_es);

			ExplainPropertyFloat("Generation", "ms",
								 1000.0 * INSTR_TIME_GET_DOUBLE(fc_ji->generation_counter),
								 3, fc_es);
			ExplainPropertyFloat("Inlining", "ms",
								 1000.0 * INSTR_TIME_GET_DOUBLE(fc_ji->inlining_counter),
								 3, fc_es);
			ExplainPropertyFloat("Optimization", "ms",
								 1000.0 * INSTR_TIME_GET_DOUBLE(fc_ji->optimization_counter),
								 3, fc_es);
			ExplainPropertyFloat("Emission", "ms",
								 1000.0 * INSTR_TIME_GET_DOUBLE(fc_ji->emission_counter),
								 3, fc_es);
			ExplainPropertyFloat("Total", "ms",
								 1000.0 * INSTR_TIME_GET_DOUBLE(fc_total_time),
								 3, fc_es);

			ExplainCloseGroup("Timing", "Timing", true, fc_es);
		}
	}

	ExplainCloseGroup("JIT", "JIT", true, fc_es);
}

/*
 * ExplainQueryText -
 *	  添加一个“查询文本”节点，包含查询的实际文本
 *
 * 调用者应已设置 *es 的选项字段，并初始化输出缓冲区 es->str。
 *
 */
void ExplainQueryText(ExplainState *fc_es, QueryDesc *fc_queryDesc)
{
	if (fc_queryDesc->sourceText)
		ExplainPropertyText("Query Text", fc_queryDesc->sourceText, fc_es);
}

/*
 * report_triggers -
 *		报告单个关系的触发器的执行统计信息
 */
static void fc_report_triggers(ResultRelInfo *fc_rInfo, bool fc_show_relname, ExplainState *fc_es)
{
	int			fc_nt;

	if (!fc_rInfo->ri_TrigDesc || !fc_rInfo->ri_TrigInstrument)
		return;
	for (fc_nt = 0; fc_nt < fc_rInfo->ri_TrigDesc->numtriggers; fc_nt++)
	{
		Trigger    *fc_trig = fc_rInfo->ri_TrigDesc->triggers + fc_nt;
		Instrumentation *fc_instr = fc_rInfo->ri_TrigInstrument + fc_nt;
		char	   *fc_relname;
		char	   *fc_conname = NULL;

		/* 必须清理工具状态 */
		InstrEndLoop(fc_instr);

		/*
		 * 我们忽略从未调用的触发器；它们很可能与
		 * 当前查询类型无关。
		 */
		if (fc_instr->ntuples == 0)
			continue;

		ExplainOpenGroup("Trigger", NULL, true, fc_es);

		fc_relname = RelationGetRelationName(fc_rInfo->ri_RelationDesc);
		if (OidIsValid(fc_trig->tgconstraint))
			fc_conname = get_constraint_name(fc_trig->tgconstraint);

		/*
		 * 在文本格式中，除非指定了 VERBOSE，否则我们避免打印触发器名称和
		 * 约束名称。在非文本格式中，我们只打印所有内容。
		 */
		if (fc_es->format == EXPLAIN_FORMAT_TEXT)
		{
			if (fc_es->verbose || fc_conname == NULL)
				appendStringInfo(fc_es->str, "Trigger %s", fc_trig->tgname);
			else
				appendStringInfoString(fc_es->str, "Trigger");
			if (fc_conname)
				appendStringInfo(fc_es->str, " for constraint %s", fc_conname);
			if (fc_show_relname)
				appendStringInfo(fc_es->str, " on %s", fc_relname);
			if (fc_es->timing)
				appendStringInfo(fc_es->str, ": time=%.3f calls=%.0f\n",
								 1000.0 * fc_instr->total, fc_instr->ntuples);
			else
				appendStringInfo(fc_es->str, ": calls=%.0f\n", fc_instr->ntuples);
		}
		else
		{
			ExplainPropertyText("Trigger Name", fc_trig->tgname, fc_es);
			if (fc_conname)
				ExplainPropertyText("Constraint Name", fc_conname, fc_es);
			ExplainPropertyText("Relation", fc_relname, fc_es);
			if (fc_es->timing)
				ExplainPropertyFloat("Time", "ms", 1000.0 * fc_instr->total, 3,
									 fc_es);
			ExplainPropertyFloat("Calls", NULL, fc_instr->ntuples, 0, fc_es);
		}

		if (fc_conname)
			pfree(fc_conname);

		ExplainCloseGroup("Trigger", NULL, true, fc_es);
	}
}

/* 计算自给定时间戳以来的经过时间（秒） */
static double fc_elapsed_time(instr_time *fc_starttime)
{
	instr_time	fc_endtime;

	INSTR_TIME_SET_CURRENT(fc_endtime);
	INSTR_TIME_SUBTRACT(fc_endtime, *fc_starttime);
	return INSTR_TIME_GET_DOUBLE(fc_endtime);
}

/*
 * ExplainPreScanNode -
 *	  对计划状态树进行预扫描，以识别引用的RTE
 *
 * 将每个引用的RTE的relid添加到*rels_used。结果控制
 * 哪些RTE由select_rtable_names_for_explain分配别名。
 * 这确保我们不会混淆性地将无后缀别名分配给从未出现在EXPLAIN输出中的RTE
 *（例如，继承父类）。
 */
static bool fc_ExplainPreScanNode(PlanState *fc_planstate, Bitmapset **fc_rels_used)
{
	Plan	   *fc_plan = fc_planstate->plan;

	switch (nodeTag(fc_plan))
	{
		case T_SeqScan:
		case T_SampleScan:
		case T_IndexScan:
		case T_IndexOnlyScan:
		case T_BitmapHeapScan:
		case T_TidScan:
		case T_TidRangeScan:
		case T_SubqueryScan:
		case T_FunctionScan:
		case T_TableFuncScan:
		case T_ValuesScan:
		case T_CteScan:
		case T_NamedTuplestoreScan:
		case T_WorkTableScan:
			*fc_rels_used = bms_add_member(*fc_rels_used,
										((Scan *) fc_plan)->scanrelid);
			break;
		case T_ForeignScan:
			*fc_rels_used = bms_add_members(*fc_rels_used,
										 ((ForeignScan *) fc_plan)->fs_relids);
			break;
		case T_CustomScan:
			*fc_rels_used = bms_add_members(*fc_rels_used,
										 ((CustomScan *) fc_plan)->custom_relids);
			break;
		case T_ModifyTable:
			*fc_rels_used = bms_add_member(*fc_rels_used,
										((ModifyTable *) fc_plan)->nominalRelation);
			if (((ModifyTable *) fc_plan)->exclRelRTI)
				*fc_rels_used = bms_add_member(*fc_rels_used,
											((ModifyTable *) fc_plan)->exclRelRTI);
			break;
		case T_Append:
			*fc_rels_used = bms_add_members(*fc_rels_used,
										 ((Append *) fc_plan)->apprelids);
			break;
		case T_MergeAppend:
			*fc_rels_used = bms_add_members(*fc_rels_used,
										 ((MergeAppend *) fc_plan)->apprelids);
			break;
		default:
			break;
	}

	return planstate_tree_walker(fc_planstate, fc_ExplainPreScanNode, fc_rels_used);
}

/*
 * ExplainNode -
 *	  将计划树的描述附加到es->str
 *
 * planstate指向当前计划节点的执行器状态节点。
 * 我们需要从PlanState节点开始，而不仅仅是Plan节点，以便
 * 获取仪器数据（如果有的话）以及子计划列表。
 *
 * ancestors是父Plan和SubPlan节点的列表，最紧密嵌套
 * 的优先。 这些是为了理解PARAM_EXEC参数所需的。
 *
 * relationship描述了此计划节点与其父节点的关系
 *（例如，“Outer”，“Inner”）；在顶层时可以为null。plan_name是一个
 * 可选的名称，将附加到节点上。
 *
 * 在文本格式中，es->indent在此函数中控制，因为我们只希望
 * 在计划节点边界处更改（但一些子例程会暂时增加它）。在非文本格式中，
 * es->indent对应于逻辑输出组的嵌套深度，因此通过
 * ExplainOpenGroup/ExplainCloseGroup进行控制。
 */
static void fc_ExplainNode(PlanState *fc_planstate, List *fc_ancestors,
			const char *fc_relationship, const char *fc_plan_name,
			ExplainState *fc_es)
{
	Plan	   *fc_plan = fc_planstate->plan;
	const char *fc_pname;			/* 文本输出的节点类型名称 */
	const char *fc_sname;			/* 非文本输出的节点类型名称 */
	const char *fc_strategy = NULL;
	const char *fc_partialmode = NULL;
	const char *fc_operation = NULL;
	const char *fc_custom_name = NULL;
	ExplainWorkersState *fc_save_workers_state = fc_es->workers_state;
	int			fc_save_indent = fc_es->indent;
	bool		fc_haschildren;

	/*
	 * 如果需要，准备每个工作者的输出缓冲区。我们将在稍后的主要输出字符串中附加这些数据。
	 */
	if (fc_planstate->worker_instrument && fc_es->analyze && !fc_es->hide_workers)
		fc_es->workers_state = fc_ExplainCreateWorkersState(fc_planstate->worker_instrument->num_workers);
	else
		fc_es->workers_state = NULL;

	/* 识别计划节点类型，并打印通用细节 */
	switch (nodeTag(fc_plan))
	{
		case T_Result:
			fc_pname = fc_sname = "Result";
			break;
		case T_ProjectSet:
			fc_pname = fc_sname = "ProjectSet";
			break;
		case T_ModifyTable:
			fc_sname = "ModifyTable";
			switch (((ModifyTable *) fc_plan)->operation)
			{
				case CMD_INSERT:
					fc_pname = fc_operation = "Insert";
					break;
				case CMD_UPDATE:
					fc_pname = fc_operation = "Update";
					break;
				case CMD_DELETE:
					fc_pname = fc_operation = "Delete";
					break;
				case CMD_MERGE:
					fc_pname = fc_operation = "Merge";
					break;
				default:
					fc_pname = "???";
					break;
			}
			break;
		case T_Append:
			fc_pname = fc_sname = "Append";
			break;
		case T_MergeAppend:
			fc_pname = fc_sname = "Merge Append";
			break;
		case T_RecursiveUnion:
			fc_pname = fc_sname = "Recursive Union";
			break;
		case T_BitmapAnd:
			fc_pname = fc_sname = "BitmapAnd";
			break;
		case T_BitmapOr:
			fc_pname = fc_sname = "BitmapOr";
			break;
		case T_NestLoop:
			fc_pname = fc_sname = "Nested Loop";
			break;
		case T_MergeJoin:
			fc_pname = "Merge";	/* “Join”通过jointype开关添加 */
			fc_sname = "Merge Join";
			break;
		case T_HashJoin:
			fc_pname = "Hash";		/* “Join”通过jointype开关添加 */
			fc_sname = "Hash Join";
			break;
		case T_SeqScan:
			fc_pname = fc_sname = "Seq Scan";
			break;
		case T_SampleScan:
			fc_pname = fc_sname = "Sample Scan";
			break;
		case T_Gather:
			fc_pname = fc_sname = "Gather";
			break;
		case T_GatherMerge:
			fc_pname = fc_sname = "Gather Merge";
			break;
		case T_IndexScan:
			fc_pname = fc_sname = "Index Scan";
			break;
		case T_IndexOnlyScan:
			fc_pname = fc_sname = "Index Only Scan";
			break;
		case T_BitmapIndexScan:
			fc_pname = fc_sname = "Bitmap Index Scan";
			break;
		case T_BitmapHeapScan:
			fc_pname = fc_sname = "Bitmap Heap Scan";
			break;
		case T_TidScan:
			fc_pname = fc_sname = "Tid Scan";
			break;
		case T_TidRangeScan:
			fc_pname = fc_sname = "Tid Range Scan";
			break;
		case T_SubqueryScan:
			fc_pname = fc_sname = "Subquery Scan";
			break;
		case T_FunctionScan:
			fc_pname = fc_sname = "Function Scan";
			break;
		case T_TableFuncScan:
			fc_pname = fc_sname = "Table Function Scan";
			break;
		case T_ValuesScan:
			fc_pname = fc_sname = "Values Scan";
			break;
		case T_CteScan:
			fc_pname = fc_sname = "CTE Scan";
			break;
		case T_NamedTuplestoreScan:
			fc_pname = fc_sname = "Named Tuplestore Scan";
			break;
		case T_WorkTableScan:
			fc_pname = fc_sname = "WorkTable Scan";
			break;
		case T_ForeignScan:
			fc_sname = "Foreign Scan";
			switch (((ForeignScan *) fc_plan)->operation)
			{
				case CMD_SELECT:
					fc_pname = "Foreign Scan";
					fc_operation = "Select";
					break;
				case CMD_INSERT:
					fc_pname = "Foreign Insert";
					fc_operation = "Insert";
					break;
				case CMD_UPDATE:
					fc_pname = "Foreign Update";
					fc_operation = "Update";
					break;
				case CMD_DELETE:
					fc_pname = "Foreign Delete";
					fc_operation = "Delete";
					break;
				default:
					fc_pname = "???";
					break;
			}
			break;
		case T_CustomScan:
			fc_sname = "Custom Scan";
			fc_custom_name = ((CustomScan *) fc_plan)->methods->CustomName;
			if (fc_custom_name)
				fc_pname = psprintf("Custom Scan (%s)", fc_custom_name);
			else
				fc_pname = fc_sname;
			break;
		case T_Material:
			fc_pname = fc_sname = "Materialize";
			break;
		case T_Memoize:
			fc_pname = fc_sname = "Memoize";
			break;
		case T_Sort:
			fc_pname = fc_sname = "Sort";
			break;
		case T_IncrementalSort:
			fc_pname = fc_sname = "Incremental Sort";
			break;
		case T_Group:
			fc_pname = fc_sname = "Group";
			break;
		case T_Agg:
			{
				Agg		   *fc_agg = (Agg *) fc_plan;

				fc_sname = "Aggregate";
				switch (fc_agg->aggstrategy)
				{
					case AGG_PLAIN:
						fc_pname = "Aggregate";
						fc_strategy = "Plain";
						break;
					case AGG_SORTED:
						fc_pname = "GroupAggregate";
						fc_strategy = "Sorted";
						break;
					case AGG_HASHED:
						fc_pname = "HashAggregate";
						fc_strategy = "Hashed";
						break;
					case AGG_MIXED:
						fc_pname = "MixedAggregate";
						fc_strategy = "Mixed";
						break;
					default:
						fc_pname = "Aggregate ???";
						fc_strategy = "???";
						break;
				}

				if (DO_AGGSPLIT_SKIPFINAL(fc_agg->aggsplit))
				{
					fc_partialmode = "Partial";
					fc_pname = psprintf("%s %s", fc_partialmode, fc_pname);
				}
				else if (DO_AGGSPLIT_COMBINE(fc_agg->aggsplit))
				{
					fc_partialmode = "Finalize";
					fc_pname = psprintf("%s %s", fc_partialmode, fc_pname);
				}
				else
					fc_partialmode = "Simple";
			}
			break;
		case T_WindowAgg:
			fc_pname = fc_sname = "WindowAgg";
			break;
		case T_Unique:
			fc_pname = fc_sname = "Unique";
			break;
		case T_SetOp:
			fc_sname = "SetOp";
			switch (((SetOp *) fc_plan)->strategy)
			{
				case SETOP_SORTED:
					fc_pname = "SetOp";
					fc_strategy = "Sorted";
					break;
				case SETOP_HASHED:
					fc_pname = "HashSetOp";
					fc_strategy = "Hashed";
					break;
				default:
					fc_pname = "SetOp ???";
					fc_strategy = "???";
					break;
			}
			break;
		case T_LockRows:
			fc_pname = fc_sname = "LockRows";
			break;
		case T_Limit:
			fc_pname = fc_sname = "Limit";
			break;
		case T_Hash:
			fc_pname = fc_sname = "Hash";
			break;
		default:
			fc_pname = fc_sname = "???";
			break;
	}

	ExplainOpenGroup("Plan",
					 fc_relationship ? NULL : "Plan",
					 true, fc_es);

	if (fc_es->format == EXPLAIN_FORMAT_TEXT)
	{
		if (fc_plan_name)
		{
			fc_ExplainIndentText(fc_es);
			appendStringInfo(fc_es->str, "%s\n", fc_plan_name);
			fc_es->indent++;
		}
		if (fc_es->indent)
		{
			fc_ExplainIndentText(fc_es);
			appendStringInfoString(fc_es->str, "->  ");
			fc_es->indent += 2;
		}
		if (fc_plan->parallel_aware)
			appendStringInfoString(fc_es->str, "Parallel ");
		if (fc_plan->async_capable)
			appendStringInfoString(fc_es->str, "Async ");
		appendStringInfoString(fc_es->str, fc_pname);
		fc_es->indent++;
	}
	else
	{
		ExplainPropertyText("Node Type", fc_sname, fc_es);
		if (fc_strategy)
			ExplainPropertyText("Strategy", fc_strategy, fc_es);
		if (fc_partialmode)
			ExplainPropertyText("Partial Mode", fc_partialmode, fc_es);
		if (fc_operation)
			ExplainPropertyText("Operation", fc_operation, fc_es);
		if (fc_relationship)
			ExplainPropertyText("Parent Relationship", fc_relationship, fc_es);
		if (fc_plan_name)
			ExplainPropertyText("Subplan Name", fc_plan_name, fc_es);
		if (fc_custom_name)
			ExplainPropertyText("Custom Plan Provider", fc_custom_name, fc_es);
		ExplainPropertyBool("Parallel Aware", fc_plan->parallel_aware, fc_es);
		ExplainPropertyBool("Async Capable", fc_plan->async_capable, fc_es);
	}

	switch (nodeTag(fc_plan))
	{
		case T_SeqScan:
		case T_SampleScan:
		case T_BitmapHeapScan:
		case T_TidScan:
		case T_TidRangeScan:
		case T_SubqueryScan:
		case T_FunctionScan:
		case T_TableFuncScan:
		case T_ValuesScan:
		case T_CteScan:
		case T_WorkTableScan:
			fc_ExplainScanTarget((Scan *) fc_plan, fc_es);
			break;
		case T_ForeignScan:
		case T_CustomScan:
			if (((Scan *) fc_plan)->scanrelid > 0)
				fc_ExplainScanTarget((Scan *) fc_plan, fc_es);
			break;
		case T_IndexScan:
			{
				IndexScan  *fc_indexscan = (IndexScan *) fc_plan;

				fc_ExplainIndexScanDetails(fc_indexscan->indexid,
										fc_indexscan->indexorderdir,
										fc_es);
				fc_ExplainScanTarget((Scan *) fc_indexscan, fc_es);
			}
			break;
		case T_IndexOnlyScan:
			{
				IndexOnlyScan *fc_indexonlyscan = (IndexOnlyScan *) fc_plan;

				fc_ExplainIndexScanDetails(fc_indexonlyscan->indexid,
										fc_indexonlyscan->indexorderdir,
										fc_es);
				fc_ExplainScanTarget((Scan *) fc_indexonlyscan, fc_es);
			}
			break;
		case T_BitmapIndexScan:
			{
				BitmapIndexScan *fc_bitmapindexscan = (BitmapIndexScan *) fc_plan;
				const char *fc_indexname =
				fc_explain_get_index_name(fc_bitmapindexscan->indexid);

				if (fc_es->format == EXPLAIN_FORMAT_TEXT)
					appendStringInfo(fc_es->str, " on %s",
									 quote_identifier(fc_indexname));
				else
					ExplainPropertyText("Index Name", fc_indexname, fc_es);
			}
			break;
		case T_ModifyTable:
			fc_ExplainModifyTarget((ModifyTable *) fc_plan, fc_es);
			break;
		case T_NestLoop:
		case T_MergeJoin:
		case T_HashJoin:
			{
				const char *fc_jointype;

				switch (((Join *) fc_plan)->jointype)
				{
					case JOIN_INNER:
						fc_jointype = "Inner";
						break;
					case JOIN_LEFT:
						fc_jointype = "Left";
						break;
					case JOIN_FULL:
						fc_jointype = "Full";
						break;
					case JOIN_RIGHT:
						fc_jointype = "Right";
						break;
					case JOIN_SEMI:
						fc_jointype = "Semi";
						break;
					case JOIN_ANTI:
						fc_jointype = "Anti";
						break;
					default:
						fc_jointype = "???";
						break;
				}
				if (fc_es->format == EXPLAIN_FORMAT_TEXT)
				{
					/*
					 * 出于历史原因，连接类型被插入到节点类型名称中...
					 */
					if (((Join *) fc_plan)->jointype != JOIN_INNER)
						appendStringInfo(fc_es->str, " %s Join", fc_jointype);
					else if (!IsA(fc_plan, NestLoop))
						appendStringInfoString(fc_es->str, " Join");
				}
				else
					ExplainPropertyText("Join Type", fc_jointype, fc_es);
			}
			break;
		case T_SetOp:
			{
				const char *fc_setopcmd;

				switch (((SetOp *) fc_plan)->cmd)
				{
					case SETOPCMD_INTERSECT:
						fc_setopcmd = "Intersect";
						break;
					case SETOPCMD_INTERSECT_ALL:
						fc_setopcmd = "Intersect All";
						break;
					case SETOPCMD_EXCEPT:
						fc_setopcmd = "Except";
						break;
					case SETOPCMD_EXCEPT_ALL:
						fc_setopcmd = "Except All";
						break;
					default:
						fc_setopcmd = "???";
						break;
				}
				if (fc_es->format == EXPLAIN_FORMAT_TEXT)
					appendStringInfo(fc_es->str, " %s", fc_setopcmd);
				else
					ExplainPropertyText("Command", fc_setopcmd, fc_es);
			}
			break;
		default:
			break;
	}

	if (fc_es->costs)
	{
		if (fc_es->format == EXPLAIN_FORMAT_TEXT)
		{
			appendStringInfo(fc_es->str, "  (cost=%.2f..%.2f rows=%.0f width=%d)",
							 fc_plan->startup_cost, fc_plan->total_cost,
							 fc_plan->plan_rows, fc_plan->plan_width);
		}
		else
		{
			ExplainPropertyFloat("Startup Cost", NULL, fc_plan->startup_cost,
								 2, fc_es);
			ExplainPropertyFloat("Total Cost", NULL, fc_plan->total_cost,
								 2, fc_es);
			ExplainPropertyFloat("Plan Rows", NULL, fc_plan->plan_rows,
								 0, fc_es);
			ExplainPropertyInteger("Plan Width", NULL, fc_plan->plan_width,
								   fc_es);
		}
	}

	/*
	 * 我们必须强制清理仪器状态，因为我们还没有执行ExecutorEnd。
	 * 这相当麻烦...
	 *
	 * 注意：contrib/auto_explain可能会导致仪器被设置
	 * 尽管我们在这里没有请求。小心不要打印用户未请求的
	 * 仪器结果。但我们还是尽可能进行InstrEndLoop调用，以减少
	 * auto_explain需要处理的情况数量。
	 */
	if (fc_planstate->instrument)
		InstrEndLoop(fc_planstate->instrument);

	if (fc_es->analyze &&
		fc_planstate->instrument && fc_planstate->instrument->nloops > 0)
	{
		double		fc_nloops = fc_planstate->instrument->nloops;
		double		fc_startup_ms = 1000.0 * fc_planstate->instrument->startup / fc_nloops;
		double		fc_total_ms = 1000.0 * fc_planstate->instrument->total / fc_nloops;
		double		fc_rows = fc_planstate->instrument->ntuples / fc_nloops;

		if (fc_es->format == EXPLAIN_FORMAT_TEXT)
		{
			if (fc_es->timing)
				appendStringInfo(fc_es->str,
								 " (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
								 fc_startup_ms, fc_total_ms, fc_rows, fc_nloops);
			else
				appendStringInfo(fc_es->str,
								 " (actual rows=%.0f loops=%.0f)",
								 fc_rows, fc_nloops);
		}
		else
		{
			if (fc_es->timing)
			{
				ExplainPropertyFloat("Actual Startup Time", "ms", fc_startup_ms,
									 3, fc_es);
				ExplainPropertyFloat("Actual Total Time", "ms", fc_total_ms,
									 3, fc_es);
			}
			ExplainPropertyFloat("Actual Rows", NULL, fc_rows, 0, fc_es);
			ExplainPropertyFloat("Actual Loops", NULL, fc_nloops, 0, fc_es);
		}
	}
	else if (fc_es->analyze)
	{
		if (fc_es->format == EXPLAIN_FORMAT_TEXT)
			appendStringInfoString(fc_es->str, " (never executed)");
		else
		{
			if (fc_es->timing)
			{
				ExplainPropertyFloat("Actual Startup Time", "ms", 0.0, 3, fc_es);
				ExplainPropertyFloat("Actual Total Time", "ms", 0.0, 3, fc_es);
			}
			ExplainPropertyFloat("Actual Rows", NULL, 0.0, 0, fc_es);
			ExplainPropertyFloat("Actual Loops", NULL, 0.0, 0, fc_es);
		}
	}

	/* 在文本格式中，第一行在这里结束 */
	if (fc_es->format == EXPLAIN_FORMAT_TEXT)
		appendStringInfoChar(fc_es->str, '\n');

	/* 准备每个工作者的一般执行细节 */
	if (fc_es->workers_state && fc_es->verbose)
	{
		WorkerInstrumentation *fc_w = fc_planstate->worker_instrument;

		for (int fc_n = 0; fc_n < fc_w->num_workers; fc_n++)
		{
			Instrumentation *fc_instrument = &fc_w->instrument[fc_n];
			double		fc_nloops = fc_instrument->nloops;
			double		fc_startup_ms;
			double		fc_total_ms;
			double		fc_rows;

			if (fc_nloops <= 0)
				continue;
			fc_startup_ms = 1000.0 * fc_instrument->startup / fc_nloops;
			fc_total_ms = 1000.0 * fc_instrument->total / fc_nloops;
			fc_rows = fc_instrument->ntuples / fc_nloops;

			fc_ExplainOpenWorker(fc_n, fc_es);

			if (fc_es->format == EXPLAIN_FORMAT_TEXT)
			{
				fc_ExplainIndentText(fc_es);
				if (fc_es->timing)
					appendStringInfo(fc_es->str,
									 "actual time=%.3f..%.3f rows=%.0f loops=%.0f\n",
									 fc_startup_ms, fc_total_ms, fc_rows, fc_nloops);
				else
					appendStringInfo(fc_es->str,
									 "actual rows=%.0f loops=%.0f\n",
									 fc_rows, fc_nloops);
			}
			else
			{
				if (fc_es->timing)
				{
					ExplainPropertyFloat("Actual Startup Time", "ms",
										 fc_startup_ms, 3, fc_es);
					ExplainPropertyFloat("Actual Total Time", "ms",
										 fc_total_ms, 3, fc_es);
				}
				ExplainPropertyFloat("Actual Rows", NULL, fc_rows, 0, fc_es);
				ExplainPropertyFloat("Actual Loops", NULL, fc_nloops, 0, fc_es);
			}

			fc_ExplainCloseWorker(fc_n, fc_es);
		}
	}

	/* 目标列表 */
	if (fc_es->verbose)
		fc_show_plan_tlist(fc_planstate, fc_ancestors, fc_es);

	/* 唯一连接 */
	switch (nodeTag(fc_plan))
	{
		case T_NestLoop:
		case T_MergeJoin:
		case T_HashJoin:
			/* 尽量不要过多描述此内容，尤其在文本模式下 */
			if (fc_es->format != EXPLAIN_FORMAT_TEXT ||
				(fc_es->verbose && ((Join *) fc_plan)->inner_unique))
				ExplainPropertyBool("Inner Unique",
									((Join *) fc_plan)->inner_unique,
									fc_es);
			break;
		default:
			break;
	}

	/* 资格、排序键等 */
	switch (nodeTag(fc_plan))
	{
		case T_IndexScan:
			fc_show_scan_qual(((IndexScan *) fc_plan)->indexqualorig,
						   "Index Cond", fc_planstate, fc_ancestors, fc_es);
			if (((IndexScan *) fc_plan)->indexqualorig)
				fc_show_instrumentation_count("Rows Removed by Index Recheck", 2,
										   fc_planstate, fc_es);
			fc_show_scan_qual(((IndexScan *) fc_plan)->indexorderbyorig,
						   "Order By", fc_planstate, fc_ancestors, fc_es);
			fc_show_scan_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
			if (fc_plan->qual)
				fc_show_instrumentation_count("Rows Removed by Filter", 1,
										   fc_planstate, fc_es);
			break;
		case T_IndexOnlyScan:
			fc_show_scan_qual(((IndexOnlyScan *) fc_plan)->indexqual,
						   "Index Cond", fc_planstate, fc_ancestors, fc_es);
			if (((IndexOnlyScan *) fc_plan)->recheckqual)
				fc_show_instrumentation_count("Rows Removed by Index Recheck", 2,
										   fc_planstate, fc_es);
			fc_show_scan_qual(((IndexOnlyScan *) fc_plan)->indexorderby,
						   "Order By", fc_planstate, fc_ancestors, fc_es);
			fc_show_scan_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
			if (fc_plan->qual)
				fc_show_instrumentation_count("Rows Removed by Filter", 1,
										   fc_planstate, fc_es);
			if (fc_es->analyze)
				ExplainPropertyFloat("Heap Fetches", NULL,
									 fc_planstate->instrument->ntuples2, 0, fc_es);
			break;
		case T_BitmapIndexScan:
			fc_show_scan_qual(((BitmapIndexScan *) fc_plan)->indexqualorig,
						   "Index Cond", fc_planstate, fc_ancestors, fc_es);
			break;
		case T_BitmapHeapScan:
			fc_show_scan_qual(((BitmapHeapScan *) fc_plan)->bitmapqualorig,
						   "Recheck Cond", fc_planstate, fc_ancestors, fc_es);
			if (((BitmapHeapScan *) fc_plan)->bitmapqualorig)
				fc_show_instrumentation_count("Rows Removed by Index Recheck", 2,
										   fc_planstate, fc_es);
			fc_show_scan_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
			if (fc_plan->qual)
				fc_show_instrumentation_count("Rows Removed by Filter", 1,
										   fc_planstate, fc_es);
			if (fc_es->analyze)
				fc_show_tidbitmap_info((BitmapHeapScanState *) fc_planstate, fc_es);
			break;
		case T_SampleScan:
			fc_show_tablesample(((SampleScan *) fc_plan)->tablesample,
							 fc_planstate, fc_ancestors, fc_es);
			/* 继续打印其他字段，与SeqScan相同 */
			/* FALLTHROUGH */
		case T_SeqScan:
		case T_ValuesScan:
		case T_CteScan:
		case T_NamedTuplestoreScan:
		case T_WorkTableScan:
		case T_SubqueryScan:
			fc_show_scan_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
			if (fc_plan->qual)
				fc_show_instrumentation_count("Rows Removed by Filter", 1,
										   fc_planstate, fc_es);
			break;
		case T_Gather:
			{
				Gather	   *fc_gather = (Gather *) fc_plan;

				fc_show_scan_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
				if (fc_plan->qual)
					fc_show_instrumentation_count("Rows Removed by Filter", 1,
											   fc_planstate, fc_es);
				ExplainPropertyInteger("Workers Planned", NULL,
									   fc_gather->num_workers, fc_es);

				/* 显示在汇聚节点评估的参数 */
				if (fc_gather->initParam)
					fc_show_eval_params(fc_gather->initParam, fc_es);

				if (fc_es->analyze)
				{
					int			fc_nworkers;

					fc_nworkers = ((GatherState *) fc_planstate)->nworkers_launched;
					ExplainPropertyInteger("Workers Launched", NULL,
										   fc_nworkers, fc_es);
				}

				if (fc_gather->single_copy || fc_es->format != EXPLAIN_FORMAT_TEXT)
					ExplainPropertyBool("Single Copy", fc_gather->single_copy, fc_es);
			}
			break;
		case T_GatherMerge:
			{
				GatherMerge *fc_gm = (GatherMerge *) fc_plan;

				fc_show_scan_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
				if (fc_plan->qual)
					fc_show_instrumentation_count("Rows Removed by Filter", 1,
											   fc_planstate, fc_es);
				ExplainPropertyInteger("Workers Planned", NULL,
									   fc_gm->num_workers, fc_es);

				/* 显示在汇聚合并节点评估的参数 */
				if (fc_gm->initParam)
					fc_show_eval_params(fc_gm->initParam, fc_es);

				if (fc_es->analyze)
				{
					int			fc_nworkers;

					fc_nworkers = ((GatherMergeState *) fc_planstate)->nworkers_launched;
					ExplainPropertyInteger("Workers Launched", NULL,
										   fc_nworkers, fc_es);
				}
			}
			break;
		case T_FunctionScan:
			if (fc_es->verbose)
			{
				List	   *fc_fexprs = NIL;
				ListCell   *fc_lc;

				foreach(fc_lc, ((FunctionScan *) fc_plan)->functions)
				{
					RangeTblFunction *fc_rtfunc = (RangeTblFunction *) lfirst(fc_lc);

					fc_fexprs = lappend(fc_fexprs, fc_rtfunc->funcexpr);
				}
				/* 我们依靠show_expression根据需要插入逗号 */
				fc_show_expression((Node *) fc_fexprs,
								"Function Call", fc_planstate, fc_ancestors,
								fc_es->verbose, fc_es);
			}
			fc_show_scan_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
			if (fc_plan->qual)
				fc_show_instrumentation_count("Rows Removed by Filter", 1,
										   fc_planstate, fc_es);
			break;
		case T_TableFuncScan:
			if (fc_es->verbose)
			{
				TableFunc  *fc_tablefunc = ((TableFuncScan *) fc_plan)->tablefunc;

				fc_show_expression((Node *) fc_tablefunc,
								"Table Function Call", fc_planstate, fc_ancestors,
								fc_es->verbose, fc_es);
			}
			fc_show_scan_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
			if (fc_plan->qual)
				fc_show_instrumentation_count("Rows Removed by Filter", 1,
										   fc_planstate, fc_es);
			break;
		case T_TidScan:
			{
				/*
				 * tidquals列表具有OR语义，因此务必将其显示为OR条件。
				 */
				List	   *fc_tidquals = ((TidScan *) fc_plan)->tidquals;

				if (list_length(fc_tidquals) > 1)
					fc_tidquals = list_make1(make_orclause(fc_tidquals));
				fc_show_scan_qual(fc_tidquals, "TID Cond", fc_planstate, fc_ancestors, fc_es);
				fc_show_scan_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
				if (fc_plan->qual)
					fc_show_instrumentation_count("Rows Removed by Filter", 1,
											   fc_planstate, fc_es);
			}
			break;
		case T_TidRangeScan:
			{
				/*
				 * tidrangequals 列表具有 AND 语义，因此务必
				 * 将其显示为 AND 条件。
				 */
				List	   *fc_tidquals = ((TidRangeScan *) fc_plan)->tidrangequals;

				if (list_length(fc_tidquals) > 1)
					fc_tidquals = list_make1(make_andclause(fc_tidquals));
				fc_show_scan_qual(fc_tidquals, "TID Cond", fc_planstate, fc_ancestors, fc_es);
				fc_show_scan_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
				if (fc_plan->qual)
					fc_show_instrumentation_count("Rows Removed by Filter", 1,
											   fc_planstate, fc_es);
			}
			break;
		case T_ForeignScan:
			fc_show_scan_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
			if (fc_plan->qual)
				fc_show_instrumentation_count("Rows Removed by Filter", 1,
										   fc_planstate, fc_es);
			fc_show_foreignscan_info((ForeignScanState *) fc_planstate, fc_es);
			break;
		case T_CustomScan:
			{
				CustomScanState *fc_css = (CustomScanState *) fc_planstate;

				fc_show_scan_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
				if (fc_plan->qual)
					fc_show_instrumentation_count("Rows Removed by Filter", 1,
											   fc_planstate, fc_es);
				if (fc_css->methods->ExplainCustomScan)
					fc_css->methods->ExplainCustomScan(fc_css, fc_ancestors, fc_es);
			}
			break;
		case T_NestLoop:
			fc_show_upper_qual(((NestLoop *) fc_plan)->join.joinqual,
							"Join Filter", fc_planstate, fc_ancestors, fc_es);
			if (((NestLoop *) fc_plan)->join.joinqual)
				fc_show_instrumentation_count("Rows Removed by Join Filter", 1,
										   fc_planstate, fc_es);
			fc_show_upper_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
			if (fc_plan->qual)
				fc_show_instrumentation_count("Rows Removed by Filter", 2,
										   fc_planstate, fc_es);
			break;
		case T_MergeJoin:
			fc_show_upper_qual(((MergeJoin *) fc_plan)->mergeclauses,
							"Merge Cond", fc_planstate, fc_ancestors, fc_es);
			fc_show_upper_qual(((MergeJoin *) fc_plan)->join.joinqual,
							"Join Filter", fc_planstate, fc_ancestors, fc_es);
			if (((MergeJoin *) fc_plan)->join.joinqual)
				fc_show_instrumentation_count("Rows Removed by Join Filter", 1,
										   fc_planstate, fc_es);
			fc_show_upper_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
			if (fc_plan->qual)
				fc_show_instrumentation_count("Rows Removed by Filter", 2,
										   fc_planstate, fc_es);
			break;
		case T_HashJoin:
			fc_show_upper_qual(((HashJoin *) fc_plan)->hashclauses,
							"Hash Cond", fc_planstate, fc_ancestors, fc_es);
			fc_show_upper_qual(((HashJoin *) fc_plan)->join.joinqual,
							"Join Filter", fc_planstate, fc_ancestors, fc_es);
			if (((HashJoin *) fc_plan)->join.joinqual)
				fc_show_instrumentation_count("Rows Removed by Join Filter", 1,
										   fc_planstate, fc_es);
			fc_show_upper_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
			if (fc_plan->qual)
				fc_show_instrumentation_count("Rows Removed by Filter", 2,
										   fc_planstate, fc_es);
			break;
		case T_Agg:
			fc_show_agg_keys(castNode(AggState, fc_planstate), fc_ancestors, fc_es);
			fc_show_upper_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
			fc_show_hashagg_info((AggState *) fc_planstate, fc_es);
			if (fc_plan->qual)
				fc_show_instrumentation_count("Rows Removed by Filter", 1,
										   fc_planstate, fc_es);
			break;
		case T_WindowAgg:
			fc_show_upper_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
			if (fc_plan->qual)
				fc_show_instrumentation_count("Rows Removed by Filter", 1,
										   fc_planstate, fc_es);
			fc_show_upper_qual(((WindowAgg *) fc_plan)->runConditionOrig,
							"Run Condition", fc_planstate, fc_ancestors, fc_es);
			break;
		case T_Group:
			fc_show_group_keys(castNode(GroupState, fc_planstate), fc_ancestors, fc_es);
			fc_show_upper_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
			if (fc_plan->qual)
				fc_show_instrumentation_count("Rows Removed by Filter", 1,
										   fc_planstate, fc_es);
			break;
		case T_Sort:
			fc_show_sort_keys(castNode(SortState, fc_planstate), fc_ancestors, fc_es);
			fc_show_sort_info(castNode(SortState, fc_planstate), fc_es);
			break;
		case T_IncrementalSort:
			fc_show_incremental_sort_keys(castNode(IncrementalSortState, fc_planstate),
									   fc_ancestors, fc_es);
			fc_show_incremental_sort_info(castNode(IncrementalSortState, fc_planstate),
									   fc_es);
			break;
		case T_MergeAppend:
			fc_show_merge_append_keys(castNode(MergeAppendState, fc_planstate),
								   fc_ancestors, fc_es);
			break;
		case T_Result:
			fc_show_upper_qual((List *) ((Result *) fc_plan)->resconstantqual,
							"One-Time Filter", fc_planstate, fc_ancestors, fc_es);
			fc_show_upper_qual(fc_plan->qual, "Filter", fc_planstate, fc_ancestors, fc_es);
			if (fc_plan->qual)
				fc_show_instrumentation_count("Rows Removed by Filter", 1,
										   fc_planstate, fc_es);
			break;
		case T_ModifyTable:
			fc_show_modifytable_info(castNode(ModifyTableState, fc_planstate), fc_ancestors,
								  fc_es);
			break;
		case T_Hash:
			fc_show_hash_info(castNode(HashState, fc_planstate), fc_es);
			break;
		case T_Memoize:
			fc_show_memoize_info(castNode(MemoizeState, fc_planstate), fc_ancestors,
							  fc_es);
			break;
		default:
			break;
	}

	/*
	 * 准备每个工作线程的 JIT 仪器。与总体 JIT
	 * 摘要一样，只有在启用了打印成本时才打印。
	 */
	if (fc_es->workers_state && fc_es->costs && fc_es->verbose)
	{
		SharedJitInstrumentation *fc_w = fc_planstate->worker_jit_instrument;

		if (fc_w)
		{
			for (int fc_n = 0; fc_n < fc_w->num_workers; fc_n++)
			{
				fc_ExplainOpenWorker(fc_n, fc_es);
				fc_ExplainPrintJIT(fc_es, fc_planstate->state->es_jit_flags,
								&fc_w->jit_instr[fc_n]);
				fc_ExplainCloseWorker(fc_n, fc_es);
			}
		}
	}

	/* 显示缓冲区/WAL 使用情况 */
	if (fc_es->buffers && fc_planstate->instrument)
		fc_show_buffer_usage(fc_es, &fc_planstate->instrument->bufusage, false);
	if (fc_es->wal && fc_planstate->instrument)
		fc_show_wal_usage(fc_es, &fc_planstate->instrument->walusage);

	/* 准备每个工作线程的缓冲区/WAL 使用情况 */
	if (fc_es->workers_state && (fc_es->buffers || fc_es->wal) && fc_es->verbose)
	{
		WorkerInstrumentation *fc_w = fc_planstate->worker_instrument;

		for (int fc_n = 0; fc_n < fc_w->num_workers; fc_n++)
		{
			Instrumentation *fc_instrument = &fc_w->instrument[fc_n];
			double		fc_nloops = fc_instrument->nloops;

			if (fc_nloops <= 0)
				continue;

			fc_ExplainOpenWorker(fc_n, fc_es);
			if (fc_es->buffers)
				fc_show_buffer_usage(fc_es, &fc_instrument->bufusage, false);
			if (fc_es->wal)
				fc_show_wal_usage(fc_es, &fc_instrument->walusage);
			fc_ExplainCloseWorker(fc_n, fc_es);
		}
	}

	/* 显示此计划节点的每个工作线程详细信息，然后弹出该堆栈 */
	if (fc_es->workers_state)
		fc_ExplainFlushWorkersState(fc_es);
	fc_es->workers_state = fc_save_workers_state;

	/*
	 * 如果在执行器初始化期间进行了分区修剪，则我们将在下面显示的
	 * 子计划数量将少于计划中指定的子计划数量。为了使这不那么
	 * 神秘，发出一个指示，说明发生了这种情况。请注意该字段现在
	 * 被发出，因为我们希望它成为父节点的一个属性；它 *不能*
	 * 在我们将要打开的 Plans 子节点中被发出。
	 */
	switch (nodeTag(fc_plan))
	{
		case T_Append:
			fc_ExplainMissingMembers(((AppendState *) fc_planstate)->as_nplans,
								  list_length(((Append *) fc_plan)->appendplans),
								  fc_es);
			break;
		case T_MergeAppend:
			fc_ExplainMissingMembers(((MergeAppendState *) fc_planstate)->ms_nplans,
								  list_length(((MergeAppend *) fc_plan)->mergeplans),
								  fc_es);
			break;
		default:
			break;
	}

	/* 准备显示子计划 */
	fc_haschildren = fc_planstate->initPlan ||
		outerPlanState(fc_planstate) ||
		innerPlanState(fc_planstate) ||
		IsA(fc_plan, Append) ||
		IsA(fc_plan, MergeAppend) ||
		IsA(fc_plan, BitmapAnd) ||
		IsA(fc_plan, BitmapOr) ||
		IsA(fc_plan, SubqueryScan) ||
		(IsA(fc_planstate, CustomScanState) &&
		 ((CustomScanState *) fc_planstate)->custom_ps != NIL) ||
		fc_planstate->subPlan;
	if (fc_haschildren)
	{
		ExplainOpenGroup("Plans", "Plans", false, fc_es);
		/* 将当前计划作为子计划的祖先列表头 */
		fc_ancestors = lcons(fc_plan, fc_ancestors);
	}

	/* initPlan-s */
	if (fc_planstate->initPlan)
		fc_ExplainSubPlans(fc_planstate->initPlan, fc_ancestors, "InitPlan", fc_es);

	/* lefttree */
	if (outerPlanState(fc_planstate))
		fc_ExplainNode(outerPlanState(fc_planstate), fc_ancestors,
					"Outer", NULL, fc_es);

	/* righttree */
	if (innerPlanState(fc_planstate))
		fc_ExplainNode(innerPlanState(fc_planstate), fc_ancestors,
					"Inner", NULL, fc_es);

	/* 特殊子计划 */
	switch (nodeTag(fc_plan))
	{
		case T_Append:
			fc_ExplainMemberNodes(((AppendState *) fc_planstate)->appendplans,
							   ((AppendState *) fc_planstate)->as_nplans,
							   fc_ancestors, fc_es);
			break;
		case T_MergeAppend:
			fc_ExplainMemberNodes(((MergeAppendState *) fc_planstate)->mergeplans,
							   ((MergeAppendState *) fc_planstate)->ms_nplans,
							   fc_ancestors, fc_es);
			break;
		case T_BitmapAnd:
			fc_ExplainMemberNodes(((BitmapAndState *) fc_planstate)->bitmapplans,
							   ((BitmapAndState *) fc_planstate)->nplans,
							   fc_ancestors, fc_es);
			break;
		case T_BitmapOr:
			fc_ExplainMemberNodes(((BitmapOrState *) fc_planstate)->bitmapplans,
							   ((BitmapOrState *) fc_planstate)->nplans,
							   fc_ancestors, fc_es);
			break;
		case T_SubqueryScan:
			fc_ExplainNode(((SubqueryScanState *) fc_planstate)->subplan, fc_ancestors,
						"Subquery", NULL, fc_es);
			break;
		case T_CustomScan:
			fc_ExplainCustomChildren((CustomScanState *) fc_planstate,
								  fc_ancestors, fc_es);
			break;
		default:
			break;
	}

	/* subPlan-s */
	if (fc_planstate->subPlan)
		fc_ExplainSubPlans(fc_planstate->subPlan, fc_ancestors, "SubPlan", fc_es);

	/* 子计划结束 */
	if (fc_haschildren)
	{
		fc_ancestors = list_delete_first(fc_ancestors);
		ExplainCloseGroup("Plans", "Plans", false, fc_es);
	}

	/* 以文本格式，撤消我们添加的任何缩进 */
	if (fc_es->format == EXPLAIN_FORMAT_TEXT)
		fc_es->indent = fc_save_indent;

	ExplainCloseGroup("Plan",
					  fc_relationship ? NULL : "Plan",
					  true, fc_es);
}

/*
 * 显示计划节点的目标列表
 */
static void fc_show_plan_tlist(PlanState *fc_planstate, List *fc_ancestors, ExplainState *fc_es)
{
	Plan	   *fc_plan = fc_planstate->plan;
	List	   *fc_context;
	List	   *fc_result = NIL;
	bool		fc_useprefix;
	ListCell   *fc_lc;

	/* 如果目标列表为空则不工作（例如在位图索引扫描中发生） */
	if (fc_plan->targetlist == NIL)
		return;
	/* Append 的目标列表没有实用价值，因此抑制它 */
	if (IsA(fc_plan, Append))
		return;
	/* MergeAppend 和 RecursiveUnion 也是如此 */
	if (IsA(fc_plan, MergeAppend))
		return;
	if (IsA(fc_plan, RecursiveUnion))
		return;

	/*
	 * 对于执行直接 INSERT/UPDATE/DELETE 的 ForeignScan 也是如此
	 *
	 * 注意：执行直接 INSERT/UPDATE 的 ForeignScan 的目标列表
	 * 可能包含在这种情况下令人困惑的子计划输出表达式。执行直接
	 * UPDATE/DELETE 的 ForeignScan 的目标列表始终包含“垃圾”目标列
	 * 以识别确切的行以更新或删除，这在这种情况下会令人困惑。
	 * 因此，我们在所有情况下都抑制它。
	 */
	if (IsA(fc_plan, ForeignScan) &&
		((ForeignScan *) fc_plan)->operation != CMD_SELECT)
		return;

	/* 设置反解析上下文 */
	fc_context = set_deparse_context_plan(fc_es->deparse_cxt,
									   fc_plan,
									   fc_ancestors);
	fc_useprefix = list_length(fc_es->rtable) > 1;

	/* 反解析每个结果列（现在包括 resjunk 列） */
	foreach(fc_lc, fc_plan->targetlist)
	{
		TargetEntry *fc_tle = (TargetEntry *) lfirst(fc_lc);

		fc_result = lappend(fc_result,
						 deparse_expression((Node *) fc_tle->expr, fc_context,
											fc_useprefix, false));
	}

	/* 打印结果 */
	ExplainPropertyList("Output", fc_result, fc_es);
}

/*
 * 显示一个通用表达式
 */
static void fc_show_expression(Node *fc_node, const char *fc_qlabel,
				PlanState *fc_planstate, List *fc_ancestors,
				bool fc_useprefix, ExplainState *fc_es)
{
	List	   *fc_context;
	char	   *fc_exprstr;

	/* 设置反解析上下文 */
	fc_context = set_deparse_context_plan(fc_es->deparse_cxt,
									   fc_planstate->plan,
									   fc_ancestors);

	/* 反解析该表达式 */
	fc_exprstr = deparse_expression(fc_node, fc_context, fc_useprefix, false);

	/* 并添加到 es->str */
	ExplainPropertyText(fc_qlabel, fc_exprstr, fc_es);
}

/*
 * 显示 qualifier 表达式（这是一个具有隐式 AND 语义的 List）
 */
static void fc_show_qual(List *fc_qual, const char *fc_qlabel,
		  PlanState *fc_planstate, List *fc_ancestors,
		  bool fc_useprefix, ExplainState *fc_es)
{
	Node	   *fc_node;

	/* 如果 qualifier 为空则不工作 */
	if (fc_qual == NIL)
		return;

	/* 将 AND 列表转换为显式 AND */
	fc_node = (Node *) make_ands_explicit(fc_qual);

	/* 并显示它 */
	fc_show_expression(fc_node, fc_qlabel, fc_planstate, fc_ancestors, fc_useprefix, fc_es);
}

/*
 * 显示扫描计划节点的 qualifier 表达式
 */
static void fc_show_scan_qual(List *fc_qual, const char *fc_qlabel,
			   PlanState *fc_planstate, List *fc_ancestors,
			   ExplainState *fc_es)
{
	bool		fc_useprefix;

	fc_useprefix = (IsA(fc_planstate->plan, SubqueryScan) || fc_es->verbose);
	fc_show_qual(fc_qual, fc_qlabel, fc_planstate, fc_ancestors, fc_useprefix, fc_es);
}

/*
 * 显示上层计划节点的 qualifier 表达式
 */
static void fc_show_upper_qual(List *fc_qual, const char *fc_qlabel,
				PlanState *fc_planstate, List *fc_ancestors,
				ExplainState *fc_es)
{
	bool		fc_useprefix;

	fc_useprefix = (list_length(fc_es->rtable) > 1 || fc_es->verbose);
	fc_show_qual(fc_qual, fc_qlabel, fc_planstate, fc_ancestors, fc_useprefix, fc_es);
}

/*
 * 显示 Sort 节点的排序键。
 */
static void fc_show_sort_keys(SortState *fc_sortstate, List *fc_ancestors, ExplainState *fc_es)
{
	Sort	   *fc_plan = (Sort *) fc_sortstate->ss.ps.plan;

	fc_show_sort_group_keys((PlanState *) fc_sortstate, "Sort Key",
						 fc_plan->numCols, 0, fc_plan->sortColIdx,
						 fc_plan->sortOperators, fc_plan->collations,
						 fc_plan->nullsFirst,
						 fc_ancestors, fc_es);
}

/*
 * 显示 IncrementalSort 节点的排序键。
 */
static void fc_show_incremental_sort_keys(IncrementalSortState *fc_incrsortstate,
						   List *fc_ancestors, ExplainState *fc_es)
{
	IncrementalSort *fc_plan = (IncrementalSort *) fc_incrsortstate->ss.ps.plan;

	fc_show_sort_group_keys((PlanState *) fc_incrsortstate, "Sort Key",
						 fc_plan->sort.numCols, fc_plan->nPresortedCols,
						 fc_plan->sort.sortColIdx,
						 fc_plan->sort.sortOperators, fc_plan->sort.collations,
						 fc_plan->sort.nullsFirst,
						 fc_ancestors, fc_es);
}

/*
 * 对于 MergeAppend 节点也是如此。
 */
static void fc_show_merge_append_keys(MergeAppendState *fc_mstate, List *fc_ancestors,
					   ExplainState *fc_es)
{
	MergeAppend *fc_plan = (MergeAppend *) fc_mstate->ps.plan;

	fc_show_sort_group_keys((PlanState *) fc_mstate, "Sort Key",
						 fc_plan->numCols, 0, fc_plan->sortColIdx,
						 fc_plan->sortOperators, fc_plan->collations,
						 fc_plan->nullsFirst,
						 fc_ancestors, fc_es);
}

/*
 * 显示 Agg 节点的分组键。
*/
static void fc_show_agg_keys(AggState *fc_astate, List *fc_ancestors,
			  ExplainState *fc_es)
{
	Agg		   *fc_plan = (Agg *) fc_astate->ss.ps.plan;

	if (fc_plan->numCols > 0 || fc_plan->groupingSets)
	{
		/* 关键列指的是子计划的目标列表 */
		fc_ancestors = lcons(fc_plan, fc_ancestors);

		if (fc_plan->groupingSets)
			fc_show_grouping_sets(outerPlanState(fc_astate), fc_plan, fc_ancestors, fc_es);
		else
			fc_show_sort_group_keys(outerPlanState(fc_astate), "Group Key",
								 fc_plan->numCols, 0, fc_plan->grpColIdx,
								 NULL, NULL, NULL,
								 fc_ancestors, fc_es);

		fc_ancestors = list_delete_first(fc_ancestors);
	}
}

static void fc_show_grouping_sets(PlanState *fc_planstate, Agg *fc_agg,
				   List *fc_ancestors, ExplainState *fc_es)
{
	List	   *fc_context;
	bool		fc_useprefix;
	ListCell   *fc_lc;

	/* 设置反解析上下文 */
	fc_context = set_deparse_context_plan(fc_es->deparse_cxt,
									   fc_planstate->plan,
									   fc_ancestors);
	fc_useprefix = (list_length(fc_es->rtable) > 1 || fc_es->verbose);

	ExplainOpenGroup("Grouping Sets", "Grouping Sets", false, fc_es);

	fc_show_grouping_set_keys(fc_planstate, fc_agg, NULL,
						   fc_context, fc_useprefix, fc_ancestors, fc_es);

	foreach(fc_lc, fc_agg->chain)
	{
		Agg		   *fc_aggnode = lfirst(fc_lc);
		Sort	   *fc_sortnode = (Sort *) fc_aggnode->plan.lefttree;

		fc_show_grouping_set_keys(fc_planstate, fc_aggnode, fc_sortnode,
							   fc_context, fc_useprefix, fc_ancestors, fc_es);
	}

	ExplainCloseGroup("Grouping Sets", "Grouping Sets", false, fc_es);
}

static void fc_show_grouping_set_keys(PlanState *fc_planstate,
					   Agg *fc_aggnode, Sort *fc_sortnode,
					   List *fc_context, bool fc_useprefix,
					   List *fc_ancestors, ExplainState *fc_es)
{
	Plan	   *fc_plan = fc_planstate->plan;
	char	   *fc_exprstr;
	ListCell   *fc_lc;
	List	   *fc_gsets = fc_aggnode->groupingSets;
	AttrNumber *fc_keycols = fc_aggnode->grpColIdx;
	const char *fc_keyname;
	const char *fc_keysetname;

	if (fc_aggnode->aggstrategy == AGG_HASHED || fc_aggnode->aggstrategy == AGG_MIXED)
	{
		fc_keyname = "Hash Key";
		fc_keysetname = "Hash Keys";
	}
	else
	{
		fc_keyname = "Group Key";
		fc_keysetname = "Group Keys";
	}

	ExplainOpenGroup("Grouping Set", NULL, true, fc_es);

	if (fc_sortnode)
	{
		fc_show_sort_group_keys(fc_planstate, "Sort Key",
							 fc_sortnode->numCols, 0, fc_sortnode->sortColIdx,
							 fc_sortnode->sortOperators, fc_sortnode->collations,
							 fc_sortnode->nullsFirst,
							 fc_ancestors, fc_es);
		if (fc_es->format == EXPLAIN_FORMAT_TEXT)
			fc_es->indent++;
	}

	ExplainOpenGroup(fc_keysetname, fc_keysetname, false, fc_es);

	foreach(fc_lc, fc_gsets)
	{
		List	   *fc_result = NIL;
		ListCell   *fc_lc2;

		foreach(fc_lc2, (List *) lfirst(fc_lc))
		{
			Index		fc_i = lfirst_int(fc_lc2);
			AttrNumber	fc_keyresno = fc_keycols[fc_i];
			TargetEntry *fc_target = get_tle_by_resno(fc_plan->targetlist,
												   fc_keyresno);

			if (!fc_target)
				elog(ERROR, "no tlist entry for key %d", fc_keyresno);
			/* 反解析表达式，显示任何顶层转换 */
			fc_exprstr = deparse_expression((Node *) fc_target->expr, fc_context,
										 fc_useprefix, true);

			fc_result = lappend(fc_result, fc_exprstr);
		}

		if (!fc_result && fc_es->format == EXPLAIN_FORMAT_TEXT)
			ExplainPropertyText(fc_keyname, "()", fc_es);
		else
			ExplainPropertyListNested(fc_keyname, fc_result, fc_es);
	}

	ExplainCloseGroup(fc_keysetname, fc_keysetname, false, fc_es);

	if (fc_sortnode && fc_es->format == EXPLAIN_FORMAT_TEXT)
		fc_es->indent--;

	ExplainCloseGroup("Grouping Set", NULL, true, fc_es);
}

/*
 * 显示 Group 节点的分组键。
 */
static void fc_show_group_keys(GroupState *fc_gstate, List *fc_ancestors,
				ExplainState *fc_es)
{
	Group	   *fc_plan = (Group *) fc_gstate->ss.ps.plan;

	/* 关键列指的是子计划的目标列表 */
	fc_ancestors = lcons(fc_plan, fc_ancestors);
	fc_show_sort_group_keys(outerPlanState(fc_gstate), "Group Key",
						 fc_plan->numCols, 0, fc_plan->grpColIdx,
						 NULL, NULL, NULL,
						 fc_ancestors, fc_es);
	fc_ancestors = list_delete_first(fc_ancestors);
}

/*
 * 显示排序/分组键的通用代码，这些键在计划节点中表示为目标列表索引的数组。
 * 如果这是一个排序键而不是分组键，还需要传递排序运算符/排序规则/NULLS FIRST 数组。
 */
static void fc_show_sort_group_keys(PlanState *fc_planstate, const char *fc_qlabel,
					 int fc_nkeys, int fc_nPresortedKeys, AttrNumber *fc_keycols,
					 Oid *fc_sortOperators, Oid *fc_collations, bool *fc_nullsFirst,
					 List *fc_ancestors, ExplainState *fc_es)
{
	Plan	   *fc_plan = fc_planstate->plan;
	List	   *fc_context;
	List	   *fc_result = NIL;
	List	   *fc_resultPresorted = NIL;
	StringInfoData fc_sortkeybuf;
	bool		fc_useprefix;
	int			fc_keyno;

	if (fc_nkeys <= 0)
		return;

	initStringInfo(&fc_sortkeybuf);

	/* 设置反解析上下文 */
	fc_context = set_deparse_context_plan(fc_es->deparse_cxt,
									   fc_plan,
									   fc_ancestors);
	fc_useprefix = (list_length(fc_es->rtable) > 1 || fc_es->verbose);

	for (fc_keyno = 0; fc_keyno < fc_nkeys; fc_keyno++)
	{
		/* 在 tlist 中查找关键表达式 */
		AttrNumber	fc_keyresno = fc_keycols[fc_keyno];
		TargetEntry *fc_target = get_tle_by_resno(fc_plan->targetlist,
											   fc_keyresno);
		char	   *fc_exprstr;

		if (!fc_target)
			elog(ERROR, "no tlist entry for key %d", fc_keyresno);
		/* 反解析表达式，显示任何顶层转换 */
		fc_exprstr = deparse_expression((Node *) fc_target->expr, fc_context,
									 fc_useprefix, true);
		resetStringInfo(&fc_sortkeybuf);
		appendStringInfoString(&fc_sortkeybuf, fc_exprstr);
		/* 如果相关，附加排序顺序信息 */
		if (fc_sortOperators != NULL)
			fc_show_sortorder_options(&fc_sortkeybuf,
								   (Node *) fc_target->expr,
								   fc_sortOperators[fc_keyno],
								   fc_collations[fc_keyno],
								   fc_nullsFirst[fc_keyno]);
		/* 每个排序键发出一个属性列表项 */
		fc_result = lappend(fc_result, pstrdup(fc_sortkeybuf.data));
		if (fc_keyno < fc_nPresortedKeys)
			fc_resultPresorted = lappend(fc_resultPresorted, fc_exprstr);
	}

	ExplainPropertyList(fc_qlabel, fc_result, fc_es);
	if (fc_nPresortedKeys > 0)
		ExplainPropertyList("Presorted Key", fc_resultPresorted, fc_es);
}

/*
 * 将列的排序顺序的非默认特性附加到 buf
 * （排序规则、方向、NULLS FIRST/LAST）
 */
static void fc_show_sortorder_options(StringInfo fc_buf, Node *fc_sortexpr,
					   Oid fc_sortOperator, Oid fc_collation, bool fc_nullsFirst)
{
	Oid			fc_sortcoltype = exprType(fc_sortexpr);
	bool		fc_reverse = false;
	TypeCacheEntry *fc_typentry;

	fc_typentry = lookup_type_cache(fc_sortcoltype,
								 TYPECACHE_LT_OPR | TYPECACHE_GT_OPR);

	/*
	 * 如果列的类型不是默认的，则打印 COLLATE。还有一些情况下这可能是多余的，例如如果表达式是其声明的排序规则的列，
	 * 但在这里很难区分（并且在这种情况下显式打印 COLLATE 仍然是个好主意）。
	 */
	if (OidIsValid(fc_collation) && fc_collation != get_typcollation(fc_sortcoltype))
	{
		char	   *fc_collname = get_collation_name(fc_collation);

		if (fc_collname == NULL)
			elog(ERROR, "cache lookup failed for collation %u", fc_collation);
		appendStringInfo(fc_buf, " COLLATE %s", quote_identifier(fc_collname));
	}

	/* 如果不是 ASC，则打印方向，如果非默认排序运算符则打印 USING */
	if (fc_sortOperator == fc_typentry->gt_opr)
	{
		appendStringInfoString(fc_buf, " DESC");
		fc_reverse = true;
	}
	else if (fc_sortOperator != fc_typentry->lt_opr)
	{
		char	   *fc_opname = get_opname(fc_sortOperator);

		if (fc_opname == NULL)
			elog(ERROR, "cache lookup failed for operator %u", fc_sortOperator);
		appendStringInfo(fc_buf, " USING %s", fc_opname);
		/* 确定操作符是被视为 ASC 还是 DESC */
		(void) get_equality_op_for_ordering_op(fc_sortOperator, &fc_reverse);
	}

	/* 仅在不为默认时添加 NULLS FIRST/LAST */
	if (fc_nullsFirst && !fc_reverse)
	{
		appendStringInfoString(fc_buf, " NULLS FIRST");
	}
	else if (!fc_nullsFirst && fc_reverse)
	{
		appendStringInfoString(fc_buf, " NULLS LAST");
	}
}

/*
 * 显示 TABLESAMPLE 属性
 */
static void fc_show_tablesample(TableSampleClause *fc_tsc, PlanState *fc_planstate,
				 List *fc_ancestors, ExplainState *fc_es)
{
	List	   *fc_context;
	bool		fc_useprefix;
	char	   *fc_method_name;
	List	   *fc_params = NIL;
	char	   *fc_repeatable;
	ListCell   *fc_lc;

	/* 设置反解析上下文 */
	fc_context = set_deparse_context_plan(fc_es->deparse_cxt,
									   fc_planstate->plan,
									   fc_ancestors);
	fc_useprefix = list_length(fc_es->rtable) > 1;

	/* 获取 tablesample 方法名称 */
	fc_method_name = get_func_name(fc_tsc->tsmhandler);

	/* 解析参数表达式 */
	foreach(fc_lc, fc_tsc->args)
	{
		Node	   *fc_arg = (Node *) lfirst(fc_lc);

		fc_params = lappend(fc_params,
						 deparse_expression(fc_arg, fc_context,
											fc_useprefix, false));
	}
	if (fc_tsc->repeatable)
		fc_repeatable = deparse_expression((Node *) fc_tsc->repeatable, fc_context,
										fc_useprefix, false);
	else
		fc_repeatable = NULL;

	/* 打印结果 */
	if (fc_es->format == EXPLAIN_FORMAT_TEXT)
	{
		bool		fc_first = true;

		fc_ExplainIndentText(fc_es);
		appendStringInfo(fc_es->str, "Sampling: %s (", fc_method_name);
		foreach(fc_lc, fc_params)
		{
			if (!fc_first)
				appendStringInfoString(fc_es->str, ", ");
			appendStringInfoString(fc_es->str, (const char *) lfirst(fc_lc));
			fc_first = false;
		}
		appendStringInfoChar(fc_es->str, ')');
		if (fc_repeatable)
			appendStringInfo(fc_es->str, " REPEATABLE (%s)", fc_repeatable);
		appendStringInfoChar(fc_es->str, '\n');
	}
	else
	{
		ExplainPropertyText("Sampling Method", fc_method_name, fc_es);
		ExplainPropertyList("Sampling Parameters", fc_params, fc_es);
		if (fc_repeatable)
			ExplainPropertyText("Repeatable Seed", fc_repeatable, fc_es);
	}
}

/*
 * 如果是 EXPLAIN ANALYZE，则显示排序节点的 tuplesort 统计信息
 */
static void fc_show_sort_info(SortState *fc_sortstate, ExplainState *fc_es)
{
	if (!fc_es->analyze)
		return;

	if (fc_sortstate->sort_Done && fc_sortstate->tuplesortstate != NULL)
	{
		Tuplesortstate *fc_state = (Tuplesortstate *) fc_sortstate->tuplesortstate;
		TuplesortInstrumentation fc_stats;
		const char *fc_sortMethod;
		const char *fc_spaceType;
		int64		fc_spaceUsed;

		tuplesort_get_stats(fc_state, &fc_stats);
		fc_sortMethod = tuplesort_method_name(fc_stats.sortMethod);
		fc_spaceType = tuplesort_space_type_name(fc_stats.spaceType);
		fc_spaceUsed = fc_stats.spaceUsed;

		if (fc_es->format == EXPLAIN_FORMAT_TEXT)
		{
			fc_ExplainIndentText(fc_es);
			appendStringInfo(fc_es->str, "Sort Method: %s  %s: " INT64_FORMAT "kB\n",
							 fc_sortMethod, fc_spaceType, fc_spaceUsed);
		}
		else
		{
			ExplainPropertyText("Sort Method", fc_sortMethod, fc_es);
			ExplainPropertyInteger("Sort Space Used", "kB", fc_spaceUsed, fc_es);
			ExplainPropertyText("Sort Space Type", fc_spaceType, fc_es);
		}
	}

	/*
	 * 你可能会认为当 es->hide_workers 为真时我们应该完全跳过这一段，但这样我们将根本无法输出排序方法的结果。
	 * 我们必须让看起来像工人 0 的数据是顶层数据。这可以通过简单地跳过 OpenWorker/CloseWorker 调用来轻松完成。
	 * 目前，我们不担心在这种情况下有多个工人的可能性；如果有，则会输出重复的字段。
	 */
	if (fc_sortstate->shared_info != NULL)
	{
		int			fc_n;

		for (fc_n = 0; fc_n < fc_sortstate->shared_info->num_workers; fc_n++)
		{
			TuplesortInstrumentation *fc_sinstrument;
			const char *fc_sortMethod;
			const char *fc_spaceType;
			int64		fc_spaceUsed;

			fc_sinstrument = &fc_sortstate->shared_info->sinstrument[fc_n];
			if (fc_sinstrument->sortMethod == SORT_TYPE_STILL_IN_PROGRESS)
				continue;		/* 忽略任何未填充的插槽 */
			fc_sortMethod = tuplesort_method_name(fc_sinstrument->sortMethod);
			fc_spaceType = tuplesort_space_type_name(fc_sinstrument->spaceType);
			fc_spaceUsed = fc_sinstrument->spaceUsed;

			if (fc_es->workers_state)
				fc_ExplainOpenWorker(fc_n, fc_es);

			if (fc_es->format == EXPLAIN_FORMAT_TEXT)
			{
				fc_ExplainIndentText(fc_es);
				appendStringInfo(fc_es->str,
								 "Sort Method: %s  %s: " INT64_FORMAT "kB\n",
								 fc_sortMethod, fc_spaceType, fc_spaceUsed);
			}
			else
			{
				ExplainPropertyText("Sort Method", fc_sortMethod, fc_es);
				ExplainPropertyInteger("Sort Space Used", "kB", fc_spaceUsed, fc_es);
				ExplainPropertyText("Sort Space Type", fc_spaceType, fc_es);
			}

			if (fc_es->workers_state)
				fc_ExplainCloseWorker(fc_n, fc_es);
		}
	}
}

/*
 * 增量排序节点按（可能非常大的数量的）批次进行排序，
 * 所以 EXPLAIN ANALYZE 需要将每个批次的 tuplesort 统计信息汇总成可理解的摘要。
 *
 * 此函数既用于非并行节点，也用于并行增量排序节点中的每个工人。
 */
static void fc_show_incremental_sort_group_info(IncrementalSortGroupInfo *fc_groupInfo,
								 const char *fc_groupLabel, bool fc_indent, ExplainState *fc_es)
{
	ListCell   *fc_methodCell;
	List	   *fc_methodNames = NIL;

	/* 生成在所有组中使用的排序方法列表。 */
	for (int fc_bit = 0; fc_bit < NUM_TUPLESORTMETHODS; fc_bit++)
	{
		TuplesortMethod fc_sortMethod = (1 << fc_bit);

		if (fc_groupInfo->sortMethods & fc_sortMethod)
		{
			const char *fc_methodName = tuplesort_method_name(fc_sortMethod);

			fc_methodNames = lappend(fc_methodNames, unconstify(char *, fc_methodName));
		}
	}

	if (fc_es->format == EXPLAIN_FORMAT_TEXT)
	{
		if (fc_indent)
			appendStringInfoSpaces(fc_es->str, fc_es->indent * 2);
		appendStringInfo(fc_es->str, "%s Groups: " INT64_FORMAT "  Sort Method", fc_groupLabel,
						 fc_groupInfo->groupCount);
		/* 根据 methodNames 的大小使用复数/单数 */
		if (list_length(fc_methodNames) > 1)
			appendStringInfoString(fc_es->str, "s: ");
		else
			appendStringInfoString(fc_es->str, ": ");
		foreach(fc_methodCell, fc_methodNames)
		{
			appendStringInfoString(fc_es->str, (char *) fc_methodCell->ptr_value);
			if (foreach_current_index(fc_methodCell) < list_length(fc_methodNames) - 1)
				appendStringInfoString(fc_es->str, ", ");
		}

		if (fc_groupInfo->maxMemorySpaceUsed > 0)
		{
			int64		fc_avgSpace = fc_groupInfo->totalMemorySpaceUsed / fc_groupInfo->groupCount;
			const char *fc_spaceTypeName;

			fc_spaceTypeName = tuplesort_space_type_name(SORT_SPACE_TYPE_MEMORY);
			appendStringInfo(fc_es->str, "  Average %s: " INT64_FORMAT "kB  Peak %s: " INT64_FORMAT "kB",
							 fc_spaceTypeName, fc_avgSpace,
							 fc_spaceTypeName, fc_groupInfo->maxMemorySpaceUsed);
		}

		if (fc_groupInfo->maxDiskSpaceUsed > 0)
		{
			int64		fc_avgSpace = fc_groupInfo->totalDiskSpaceUsed / fc_groupInfo->groupCount;

			const char *fc_spaceTypeName;

			fc_spaceTypeName = tuplesort_space_type_name(SORT_SPACE_TYPE_DISK);
			appendStringInfo(fc_es->str, "  Average %s: " INT64_FORMAT "kB  Peak %s: " INT64_FORMAT "kB",
							 fc_spaceTypeName, fc_avgSpace,
							 fc_spaceTypeName, fc_groupInfo->maxDiskSpaceUsed);
		}
	}
	else
	{
		StringInfoData fc_groupName;

		initStringInfo(&fc_groupName);
		appendStringInfo(&fc_groupName, "%s Groups", fc_groupLabel);
		ExplainOpenGroup("Incremental Sort Groups", fc_groupName.data, true, fc_es);
		ExplainPropertyInteger("Group Count", NULL, fc_groupInfo->groupCount, fc_es);

		ExplainPropertyList("Sort Methods Used", fc_methodNames, fc_es);

		if (fc_groupInfo->maxMemorySpaceUsed > 0)
		{
			int64		fc_avgSpace = fc_groupInfo->totalMemorySpaceUsed / fc_groupInfo->groupCount;
			const char *fc_spaceTypeName;
			StringInfoData fc_memoryName;

			fc_spaceTypeName = tuplesort_space_type_name(SORT_SPACE_TYPE_MEMORY);
			initStringInfo(&fc_memoryName);
			appendStringInfo(&fc_memoryName, "Sort Space %s", fc_spaceTypeName);
			ExplainOpenGroup("Sort Space", fc_memoryName.data, true, fc_es);

			ExplainPropertyInteger("Average Sort Space Used", "kB", fc_avgSpace, fc_es);
			ExplainPropertyInteger("Peak Sort Space Used", "kB",
								   fc_groupInfo->maxMemorySpaceUsed, fc_es);

			ExplainCloseGroup("Sort Space", fc_memoryName.data, true, fc_es);
		}
		if (fc_groupInfo->maxDiskSpaceUsed > 0)
		{
			int64		fc_avgSpace = fc_groupInfo->totalDiskSpaceUsed / fc_groupInfo->groupCount;
			const char *fc_spaceTypeName;
			StringInfoData fc_diskName;

			fc_spaceTypeName = tuplesort_space_type_name(SORT_SPACE_TYPE_DISK);
			initStringInfo(&fc_diskName);
			appendStringInfo(&fc_diskName, "Sort Space %s", fc_spaceTypeName);
			ExplainOpenGroup("Sort Space", fc_diskName.data, true, fc_es);

			ExplainPropertyInteger("Average Sort Space Used", "kB", fc_avgSpace, fc_es);
			ExplainPropertyInteger("Peak Sort Space Used", "kB",
								   fc_groupInfo->maxDiskSpaceUsed, fc_es);

			ExplainCloseGroup("Sort Space", fc_diskName.data, true, fc_es);
		}

		ExplainCloseGroup("Incremental Sort Groups", fc_groupName.data, true, fc_es);
	}
}

/*
 * 如果是 EXPLAIN ANALYZE，则显示增量排序节点的 tuplesort 统计信息
 */
static void fc_show_incremental_sort_info(IncrementalSortState *fc_incrsortstate,
						   ExplainState *fc_es)
{
	IncrementalSortGroupInfo *fc_fullsortGroupInfo;
	IncrementalSortGroupInfo *fc_prefixsortGroupInfo;

	fc_fullsortGroupInfo = &fc_incrsortstate->incsort_info.fullsortGroupInfo;

	if (!fc_es->analyze)
		return;

	/*
	 * 由于除非我们首先对完整的组进行排序并转换模式（将元组复制到前缀组），
	 * 否则我们永远不会有任何前缀组，所以如果没有完整的组，我们不需要做任何事情。
	 *
	 * 然而，如果没有完整的组，我们仍然必须在此块之后继续，
	 * 因为即使领导者没有参与，也可能有真正工作的工人。
	 */
	if (fc_fullsortGroupInfo->groupCount > 0)
	{
		fc_show_incremental_sort_group_info(fc_fullsortGroupInfo, "Full-sort", true, fc_es);
		fc_prefixsortGroupInfo = &fc_incrsortstate->incsort_info.prefixsortGroupInfo;
		if (fc_prefixsortGroupInfo->groupCount > 0)
		{
			if (fc_es->format == EXPLAIN_FORMAT_TEXT)
				appendStringInfoChar(fc_es->str, '\n');
			fc_show_incremental_sort_group_info(fc_prefixsortGroupInfo, "Pre-sorted", true, fc_es);
		}
		if (fc_es->format == EXPLAIN_FORMAT_TEXT)
			appendStringInfoChar(fc_es->str, '\n');
	}

	if (fc_incrsortstate->shared_info != NULL)
	{
		int			fc_n;
		bool		fc_indent_first_line;

		for (fc_n = 0; fc_n < fc_incrsortstate->shared_info->num_workers; fc_n++)
		{
			IncrementalSortInfo *fc_incsort_info =
			&fc_incrsortstate->shared_info->sinfo[fc_n];

			/*
			 * 如果一个 worker 根本没有处理任何排序组，那么
			 * 就将其排除在输出之外，因为它要么没有启动，要么没有
			 * 贡献任何有意义的内容。
			 */
			fc_fullsortGroupInfo = &fc_incsort_info->fullsortGroupInfo;

			/*
			 * 由于我们在排序完整组并转换模式（将元组复制到前缀组）之前
			 * 从不拥有任何前缀组，因此如果没有完整组，我们不需要
			 * 做任何事情。
			 */
			if (fc_fullsortGroupInfo->groupCount == 0)
				continue;

			if (fc_es->workers_state)
				fc_ExplainOpenWorker(fc_n, fc_es);

			fc_indent_first_line = fc_es->workers_state == NULL || fc_es->verbose;
			fc_show_incremental_sort_group_info(fc_fullsortGroupInfo, "Full-sort",
											 fc_indent_first_line, fc_es);
			fc_prefixsortGroupInfo = &fc_incsort_info->prefixsortGroupInfo;
			if (fc_prefixsortGroupInfo->groupCount > 0)
			{
				if (fc_es->format == EXPLAIN_FORMAT_TEXT)
					appendStringInfoChar(fc_es->str, '\n');
				fc_show_incremental_sort_group_info(fc_prefixsortGroupInfo, "Pre-sorted", true, fc_es);
			}
			if (fc_es->format == EXPLAIN_FORMAT_TEXT)
				appendStringInfoChar(fc_es->str, '\n');

			if (fc_es->workers_state)
				fc_ExplainCloseWorker(fc_n, fc_es);
		}
	}
}

/*
 * 显示哈希桶/批次的信息。
 */
static void fc_show_hash_info(HashState *fc_hashstate, ExplainState *fc_es)
{
	HashInstrumentation fc_hinstrument = {0};

	/*
	 * 从本地进程收集统计信息，即使它是并行查询。
	 * 在并行查询中，领导进程可能已经运行了
	 * 哈希连接，也可能没有，即使它运行过，由于
	 * 时机问题（如果启动得晚，它可能在外部
	 * 关系中没有看到任何元组，因此跳过了构建哈希表）。因此，我们必须
	 * 准备从所有参与者获取仪表数据。
	 */
	if (fc_hashstate->hinstrument)
		memcpy(&fc_hinstrument, fc_hashstate->hinstrument,
			   sizeof(HashInstrumentation));

	/*
	 * 合并 workers 的结果。在并行无意识的情况下，
	 * 所有参与者的结果应该是相同的，除非
	 * 参与者根本没有执行连接因此没有数据。在
	 * 并行感知的情况下，我们需要考虑所有结果。
	 * 每个 worker 可能看到了不同的批次子集，我们想要报告
	 * 所有批次中最高的内存使用情况。我们也取其他
	 * 值的最大值，原因同 ExecHashAccumInstrumentation。
	 */
	if (fc_hashstate->shared_info)
	{
		SharedHashInfo *fc_shared_info = fc_hashstate->shared_info;
		int			fc_i;

		for (fc_i = 0; fc_i < fc_shared_info->num_workers; ++fc_i)
		{
			HashInstrumentation *fc_worker_hi = &fc_shared_info->hinstrument[fc_i];

			fc_hinstrument.nbuckets = Max(fc_hinstrument.nbuckets,
									   fc_worker_hi->nbuckets);
			fc_hinstrument.nbuckets_original = Max(fc_hinstrument.nbuckets_original,
												fc_worker_hi->nbuckets_original);
			fc_hinstrument.nbatch = Max(fc_hinstrument.nbatch,
									 fc_worker_hi->nbatch);
			fc_hinstrument.nbatch_original = Max(fc_hinstrument.nbatch_original,
											  fc_worker_hi->nbatch_original);
			fc_hinstrument.space_peak = Max(fc_hinstrument.space_peak,
										 fc_worker_hi->space_peak);
		}
	}

	if (fc_hinstrument.nbatch > 0)
	{
		long		fc_spacePeakKb = (fc_hinstrument.space_peak + 1023) / 1024;

		if (fc_es->format != EXPLAIN_FORMAT_TEXT)
		{
			ExplainPropertyInteger("Hash Buckets", NULL,
								   fc_hinstrument.nbuckets, fc_es);
			ExplainPropertyInteger("Original Hash Buckets", NULL,
								   fc_hinstrument.nbuckets_original, fc_es);
			ExplainPropertyInteger("Hash Batches", NULL,
								   fc_hinstrument.nbatch, fc_es);
			ExplainPropertyInteger("Original Hash Batches", NULL,
								   fc_hinstrument.nbatch_original, fc_es);
			ExplainPropertyInteger("Peak Memory Usage", "kB",
								   fc_spacePeakKb, fc_es);
		}
		else if (fc_hinstrument.nbatch_original != fc_hinstrument.nbatch ||
				 fc_hinstrument.nbuckets_original != fc_hinstrument.nbuckets)
		{
			fc_ExplainIndentText(fc_es);
			appendStringInfo(fc_es->str,
							 "Buckets: %d (originally %d)  Batches: %d (originally %d)  Memory Usage: %ldkB\n",
							 fc_hinstrument.nbuckets,
							 fc_hinstrument.nbuckets_original,
							 fc_hinstrument.nbatch,
							 fc_hinstrument.nbatch_original,
							 fc_spacePeakKb);
		}
		else
		{
			fc_ExplainIndentText(fc_es);
			appendStringInfo(fc_es->str,
							 "Buckets: %d  Batches: %d  Memory Usage: %ldkB\n",
							 fc_hinstrument.nbuckets, fc_hinstrument.nbatch,
							 fc_spacePeakKb);
		}
	}
}

/*
 * 显示关于记忆命中/未命中/驱逐和内存使用情况的信息。
 */
static void fc_show_memoize_info(MemoizeState *fc_mstate, List *fc_ancestors, ExplainState *fc_es)
{
	Plan	   *fc_plan = ((PlanState *) fc_mstate)->plan;
	ListCell   *fc_lc;
	List	   *fc_context;
	StringInfoData fc_keystr;
	char	   *fc_separator = "";
	bool		fc_useprefix;
	int64		fc_memPeakKb;

	initStringInfo(&fc_keystr);

	/*
	 * 很难想象有一个记忆节点少于 2 个 RTE，但
	 * 让我们在本文件的其他地方保持相同的 useprefix 逻辑。
	 */
	fc_useprefix = list_length(fc_es->rtable) > 1 || fc_es->verbose;

	/* 设置反解析上下文 */
	fc_context = set_deparse_context_plan(fc_es->deparse_cxt,
									   fc_plan,
									   fc_ancestors);

	foreach(fc_lc, ((Memoize *) fc_plan)->param_exprs)
	{
		Node	   *fc_expr = (Node *) lfirst(fc_lc);

		appendStringInfoString(&fc_keystr, fc_separator);

		appendStringInfoString(&fc_keystr, deparse_expression(fc_expr, fc_context,
														   fc_useprefix, false));
		fc_separator = ", ";
	}

	if (fc_es->format != EXPLAIN_FORMAT_TEXT)
	{
		ExplainPropertyText("Cache Key", fc_keystr.data, fc_es);
		ExplainPropertyText("Cache Mode", fc_mstate->binary_mode ? "binary" : "logical", fc_es);
	}
	else
	{
		fc_ExplainIndentText(fc_es);
		appendStringInfo(fc_es->str, "Cache Key: %s\n", fc_keystr.data);
		fc_ExplainIndentText(fc_es);
		appendStringInfo(fc_es->str, "Cache Mode: %s\n", fc_mstate->binary_mode ? "binary" : "logical");
	}

	pfree(fc_keystr.data);

	if (!fc_es->analyze)
		return;

	if (fc_mstate->stats.cache_misses > 0)
	{
		/*
		 * mem_peak 仅在我们释放内存时设置，因此在 mem_peak 为 0 时
		 * 我们必须使用 mem_used。
		 */
		if (fc_mstate->stats.mem_peak > 0)
			fc_memPeakKb = (fc_mstate->stats.mem_peak + 1023) / 1024;
		else
			fc_memPeakKb = (fc_mstate->mem_used + 1023) / 1024;

		if (fc_es->format != EXPLAIN_FORMAT_TEXT)
		{
			ExplainPropertyInteger("Cache Hits", NULL, fc_mstate->stats.cache_hits, fc_es);
			ExplainPropertyInteger("Cache Misses", NULL, fc_mstate->stats.cache_misses, fc_es);
			ExplainPropertyInteger("Cache Evictions", NULL, fc_mstate->stats.cache_evictions, fc_es);
			ExplainPropertyInteger("Cache Overflows", NULL, fc_mstate->stats.cache_overflows, fc_es);
			ExplainPropertyInteger("Peak Memory Usage", "kB", fc_memPeakKb, fc_es);
		}
		else
		{
			fc_ExplainIndentText(fc_es);
			appendStringInfo(fc_es->str,
							 "Hits: " UINT64_FORMAT "  Misses: " UINT64_FORMAT "  Evictions: " UINT64_FORMAT "  Overflows: " UINT64_FORMAT "  Memory Usage: " INT64_FORMAT "kB\n",
							 fc_mstate->stats.cache_hits,
							 fc_mstate->stats.cache_misses,
							 fc_mstate->stats.cache_evictions,
							 fc_mstate->stats.cache_overflows,
							 fc_memPeakKb);
		}
	}

	if (fc_mstate->shared_info == NULL)
		return;

	/* 显示来自并行 workers 的详细信息 */
	for (int fc_n = 0; fc_n < fc_mstate->shared_info->num_workers; fc_n++)
	{
		MemoizeInstrumentation *fc_si;

		fc_si = &fc_mstate->shared_info->sinstrument[fc_n];

		/*
		 * 跳过没有任何工作的 workers。我们不必检查
		 * 缓存命中，因为未命中总是在缓存命中之前发生。
		 */
		if (fc_si->cache_misses == 0)
			continue;

		if (fc_es->workers_state)
			fc_ExplainOpenWorker(fc_n, fc_es);

		/*
		 * 由于 worker 的 MemoizeState.mem_used 字段对
		 * 我们不可用，ExecEndMemoize 将为我们设置
		 * MemoizeInstrumentation.mem_peak 字段。无需进行像上面
		 * 串行情况的零检查。
		 */
		fc_memPeakKb = (fc_si->mem_peak + 1023) / 1024;

		if (fc_es->format == EXPLAIN_FORMAT_TEXT)
		{
			fc_ExplainIndentText(fc_es);
			appendStringInfo(fc_es->str,
							 "Hits: " UINT64_FORMAT "  Misses: " UINT64_FORMAT "  Evictions: " UINT64_FORMAT "  Overflows: " UINT64_FORMAT "  Memory Usage: " INT64_FORMAT "kB\n",
							 fc_si->cache_hits, fc_si->cache_misses,
							 fc_si->cache_evictions, fc_si->cache_overflows,
							 fc_memPeakKb);
		}
		else
		{
			ExplainPropertyInteger("Cache Hits", NULL,
								   fc_si->cache_hits, fc_es);
			ExplainPropertyInteger("Cache Misses", NULL,
								   fc_si->cache_misses, fc_es);
			ExplainPropertyInteger("Cache Evictions", NULL,
								   fc_si->cache_evictions, fc_es);
			ExplainPropertyInteger("Cache Overflows", NULL,
								   fc_si->cache_overflows, fc_es);
			ExplainPropertyInteger("Peak Memory Usage", "kB", fc_memPeakKb,
								   fc_es);
		}

		if (fc_es->workers_state)
			fc_ExplainCloseWorker(fc_n, fc_es);
	}
}

/*
 * 显示哈希聚合内存使用情况和批次的信息。
 */
static void fc_show_hashagg_info(AggState *fc_aggstate, ExplainState *fc_es)
{
	Agg		   *fc_agg = (Agg *) fc_aggstate->ss.ps.plan;
	int64		fc_memPeakKb = (fc_aggstate->hash_mem_peak + 1023) / 1024;

	if (fc_agg->aggstrategy != AGG_HASHED &&
		fc_agg->aggstrategy != AGG_MIXED)
		return;

	if (fc_es->format != EXPLAIN_FORMAT_TEXT)
	{

		if (fc_es->costs)
			ExplainPropertyInteger("Planned Partitions", NULL,
								   fc_aggstate->hash_planned_partitions, fc_es);

		/*
		 * 在并行查询期间，领导者可能没有提供帮助。
		 * 我们通过检查它使用了多少内存来检测这一点。
		 * 如果我们发现它没有做任何工作，那么我们就不显示它的属性。
		 */
		if (fc_es->analyze && fc_aggstate->hash_mem_peak > 0)
		{
			ExplainPropertyInteger("HashAgg Batches", NULL,
								   fc_aggstate->hash_batches_used, fc_es);
			ExplainPropertyInteger("Peak Memory Usage", "kB", fc_memPeakKb, fc_es);
			ExplainPropertyInteger("Disk Usage", "kB",
								   fc_aggstate->hash_disk_used, fc_es);
		}
	}
	else
	{
		bool		fc_gotone = false;

		if (fc_es->costs && fc_aggstate->hash_planned_partitions > 0)
		{
			fc_ExplainIndentText(fc_es);
			appendStringInfo(fc_es->str, "Planned Partitions: %d",
							 fc_aggstate->hash_planned_partitions);
			fc_gotone = true;
		}

		/*
		 * 在并行查询期间，领导者可能没有提供帮助。
		 * 我们通过检查它使用了多少内存来检测这一点。
		 * 如果我们发现它没有做任何工作，那么我们就不显示它的属性。
		 */
		if (fc_es->analyze && fc_aggstate->hash_mem_peak > 0)
		{
			if (!fc_gotone)
				fc_ExplainIndentText(fc_es);
			else
				appendStringInfoString(fc_es->str, "  ");

			appendStringInfo(fc_es->str, "Batches: %d  Memory Usage: " INT64_FORMAT "kB",
							 fc_aggstate->hash_batches_used, fc_memPeakKb);
			fc_gotone = true;

			/* 仅在我们溢出到磁盘时显示磁盘使用情况 */
			if (fc_aggstate->hash_batches_used > 1)
			{
				appendStringInfo(fc_es->str, "  Disk Usage: " UINT64_FORMAT "kB",
								 fc_aggstate->hash_disk_used);
			}
		}

		if (fc_gotone)
			appendStringInfoChar(fc_es->str, '\n');
	}

	/* 显示每个并行 worker 的统计信息 */
	if (fc_es->analyze && fc_aggstate->shared_info != NULL)
	{
		for (int fc_n = 0; fc_n < fc_aggstate->shared_info->num_workers; fc_n++)
		{
			AggregateInstrumentation *fc_sinstrument;
			uint64		fc_hash_disk_used;
			int			fc_hash_batches_used;

			fc_sinstrument = &fc_aggstate->shared_info->sinstrument[fc_n];
			/* 跳过没有做任何事情的 workers */
			if (fc_sinstrument->hash_mem_peak == 0)
				continue;
			fc_hash_disk_used = fc_sinstrument->hash_disk_used;
			fc_hash_batches_used = fc_sinstrument->hash_batches_used;
			fc_memPeakKb = (fc_sinstrument->hash_mem_peak + 1023) / 1024;

			if (fc_es->workers_state)
				fc_ExplainOpenWorker(fc_n, fc_es);

			if (fc_es->format == EXPLAIN_FORMAT_TEXT)
			{
				fc_ExplainIndentText(fc_es);

				appendStringInfo(fc_es->str, "Batches: %d  Memory Usage: " INT64_FORMAT "kB",
								 fc_hash_batches_used, fc_memPeakKb);

				/* 仅在我们溢出到磁盘时显示磁盘使用情况 */
				if (fc_hash_batches_used > 1)
					appendStringInfo(fc_es->str, "  Disk Usage: " UINT64_FORMAT "kB",
									 fc_hash_disk_used);
				appendStringInfoChar(fc_es->str, '\n');
			}
			else
			{
				ExplainPropertyInteger("HashAgg Batches", NULL,
									   fc_hash_batches_used, fc_es);
				ExplainPropertyInteger("Peak Memory Usage", "kB", fc_memPeakKb,
									   fc_es);
				ExplainPropertyInteger("Disk Usage", "kB", fc_hash_disk_used, fc_es);
			}

			if (fc_es->workers_state)
				fc_ExplainCloseWorker(fc_n, fc_es);
		}
	}
}

/*
 * 如果是 EXPLAIN ANALYZE，显示 BitmapHeapScan 节点的准确/损失页面
 */
static void fc_show_tidbitmap_info(BitmapHeapScanState *fc_planstate, ExplainState *fc_es)
{
	if (fc_es->format != EXPLAIN_FORMAT_TEXT)
	{
		ExplainPropertyInteger("Exact Heap Blocks", NULL,
							   fc_planstate->exact_pages, fc_es);
		ExplainPropertyInteger("Lossy Heap Blocks", NULL,
							   fc_planstate->lossy_pages, fc_es);
	}
	else
	{
		if (fc_planstate->exact_pages > 0 || fc_planstate->lossy_pages > 0)
		{
			fc_ExplainIndentText(fc_es);
			appendStringInfoString(fc_es->str, "Heap Blocks:");
			if (fc_planstate->exact_pages > 0)
				appendStringInfo(fc_es->str, " exact=%ld", fc_planstate->exact_pages);
			if (fc_planstate->lossy_pages > 0)
				appendStringInfo(fc_es->str, " lossy=%ld", fc_planstate->lossy_pages);
			appendStringInfoChar(fc_es->str, '\n');
		}
	}
}

/*
 * 如果是 EXPLAIN ANALYZE，显示计划节点的仪表信息
 *
 * "which" 确定要打印哪个仪表计数器
 */
static void fc_show_instrumentation_count(const char *fc_qlabel, int fc_which,
						   PlanState *fc_planstate, ExplainState *fc_es)
{
	double		fc_nfiltered;
	double		fc_nloops;

	if (!fc_es->analyze || !fc_planstate->instrument)
		return;

	if (fc_which == 2)
		fc_nfiltered = fc_planstate->instrument->nfiltered2;
	else
		fc_nfiltered = fc_planstate->instrument->nfiltered1;
	fc_nloops = fc_planstate->instrument->nloops;

	/* 在文本模式下，抑制零计数；它们不够有趣 */
	if (fc_nfiltered > 0 || fc_es->format != EXPLAIN_FORMAT_TEXT)
	{
		if (fc_nloops > 0)
			ExplainPropertyFloat(fc_qlabel, NULL, fc_nfiltered / fc_nloops, 0, fc_es);
		else
			ExplainPropertyFloat(fc_qlabel, NULL, 0.0, 0, fc_es);
	}
}

/*
 * 显示 ForeignScan 节点的额外信息。
 */
static void fc_show_foreignscan_info(ForeignScanState *fc_fsstate, ExplainState *fc_es)
{
	FdwRoutine *fc_fdwroutine = fc_fsstate->fdwroutine;

	/* 让 FDW 输出它想要的字段 */
	if (((ForeignScan *) fc_fsstate->ss.ps.plan)->operation != CMD_SELECT)
	{
		if (fc_fdwroutine->ExplainDirectModify != NULL)
			fc_fdwroutine->ExplainDirectModify(fc_fsstate, fc_es);
	}
	else
	{
		if (fc_fdwroutine->ExplainForeignScan != NULL)
			fc_fdwroutine->ExplainForeignScan(fc_fsstate, fc_es);
	}
}

/*
 * 显示在 Gather 或 Gather Merge 节点处评估的 initplan 参数。
 */
static void fc_show_eval_params(Bitmapset *fc_bms_params, ExplainState *fc_es)
{
	int			fc_paramid = -1;
	List	   *fc_params = NIL;

	Assert(fc_bms_params);

	while ((fc_paramid = bms_next_member(fc_bms_params, fc_paramid)) >= 0)
	{
		char		fc_param[32];

		snprintf(fc_param, sizeof(fc_param), "$%d", fc_paramid);
		fc_params = lappend(fc_params, pstrdup(fc_param));
	}

	if (fc_params)
		ExplainPropertyList("Params Evaluated", fc_params, fc_es);
}

/*
 * 在 EXPLAIN 中获取索引的名称
 *
 * 我们允许插件在这里控制，以便可以解释涉及假设索引的计划。
 *
 * 注意：此函数返回的名称应为“原始”；调用者将根据需要应用引用。以前的约定是在这里进行引用，但我们在非文本输出格式中不希望这样。
 */
static const char * fc_explain_get_index_name(Oid fc_indexId)
{
	const char *fc_result;

	if (explain_get_index_name_hook)
		fc_result = (*explain_get_index_name_hook) (fc_indexId);
	else
		fc_result = NULL;
	if (fc_result == NULL)
	{
		/* 默认行为：在目录中查找 */
		fc_result = get_rel_name(fc_indexId);
		if (fc_result == NULL)
			elog(ERROR, "cache lookup failed for index %u", fc_indexId);
	}
	return fc_result;
}

/*
 * 显示缓冲区使用情况的详细信息。
 */
static void fc_show_buffer_usage(ExplainState *fc_es, const BufferUsage *fc_usage, bool fc_planning)
{
	if (fc_es->format == EXPLAIN_FORMAT_TEXT)
	{
		bool		fc_has_shared = (fc_usage->shared_blks_hit > 0 ||
								  fc_usage->shared_blks_read > 0 ||
								  fc_usage->shared_blks_dirtied > 0 ||
								  fc_usage->shared_blks_written > 0);
		bool		fc_has_local = (fc_usage->local_blks_hit > 0 ||
								 fc_usage->local_blks_read > 0 ||
								 fc_usage->local_blks_dirtied > 0 ||
								 fc_usage->local_blks_written > 0);
		bool		fc_has_temp = (fc_usage->temp_blks_read > 0 ||
								fc_usage->temp_blks_written > 0);
		bool		fc_has_timing = (!INSTR_TIME_IS_ZERO(fc_usage->blk_read_time) ||
								  !INSTR_TIME_IS_ZERO(fc_usage->blk_write_time));
		bool		fc_has_temp_timing = (!INSTR_TIME_IS_ZERO(fc_usage->temp_blk_read_time) ||
									   !INSTR_TIME_IS_ZERO(fc_usage->temp_blk_write_time));
		bool		fc_show_planning = (fc_planning && (fc_has_shared ||
												  fc_has_local || fc_has_temp || fc_has_timing ||
												  fc_has_temp_timing));

		if (fc_show_planning)
		{
			fc_ExplainIndentText(fc_es);
			appendStringInfoString(fc_es->str, "Planning:\n");
			fc_es->indent++;
		}

		/* 仅显示正计数值。 */
		if (fc_has_shared || fc_has_local || fc_has_temp)
		{
			fc_ExplainIndentText(fc_es);
			appendStringInfoString(fc_es->str, "Buffers:");

			if (fc_has_shared)
			{
				appendStringInfoString(fc_es->str, " shared");
				if (fc_usage->shared_blks_hit > 0)
					appendStringInfo(fc_es->str, " hit=%lld",
									 (long long) fc_usage->shared_blks_hit);
				if (fc_usage->shared_blks_read > 0)
					appendStringInfo(fc_es->str, " read=%lld",
									 (long long) fc_usage->shared_blks_read);
				if (fc_usage->shared_blks_dirtied > 0)
					appendStringInfo(fc_es->str, " dirtied=%lld",
									 (long long) fc_usage->shared_blks_dirtied);
				if (fc_usage->shared_blks_written > 0)
					appendStringInfo(fc_es->str, " written=%lld",
									 (long long) fc_usage->shared_blks_written);
				if (fc_has_local || fc_has_temp)
					appendStringInfoChar(fc_es->str, ',');
			}
			if (fc_has_local)
			{
				appendStringInfoString(fc_es->str, " local");
				if (fc_usage->local_blks_hit > 0)
					appendStringInfo(fc_es->str, " hit=%lld",
									 (long long) fc_usage->local_blks_hit);
				if (fc_usage->local_blks_read > 0)
					appendStringInfo(fc_es->str, " read=%lld",
									 (long long) fc_usage->local_blks_read);
				if (fc_usage->local_blks_dirtied > 0)
					appendStringInfo(fc_es->str, " dirtied=%lld",
									 (long long) fc_usage->local_blks_dirtied);
				if (fc_usage->local_blks_written > 0)
					appendStringInfo(fc_es->str, " written=%lld",
									 (long long) fc_usage->local_blks_written);
				if (fc_has_temp)
					appendStringInfoChar(fc_es->str, ',');
			}
			if (fc_has_temp)
			{
				appendStringInfoString(fc_es->str, " temp");
				if (fc_usage->temp_blks_read > 0)
					appendStringInfo(fc_es->str, " read=%lld",
									 (long long) fc_usage->temp_blks_read);
				if (fc_usage->temp_blks_written > 0)
					appendStringInfo(fc_es->str, " written=%lld",
									 (long long) fc_usage->temp_blks_written);
			}
			appendStringInfoChar(fc_es->str, '\n');
		}

		/* 如上所述，仅显示正计数值。 */
		if (fc_has_timing || fc_has_temp_timing)
		{
			fc_ExplainIndentText(fc_es);
			appendStringInfoString(fc_es->str, "I/O Timings:");

			if (fc_has_timing)
			{
				appendStringInfoString(fc_es->str, " shared");
				if (!INSTR_TIME_IS_ZERO(fc_usage->blk_read_time))
					appendStringInfo(fc_es->str, " read=%0.3f",
									 INSTR_TIME_GET_MILLISEC(fc_usage->blk_read_time));
				if (!INSTR_TIME_IS_ZERO(fc_usage->blk_write_time))
					appendStringInfo(fc_es->str, " write=%0.3f",
									 INSTR_TIME_GET_MILLISEC(fc_usage->blk_write_time));
				if (fc_has_temp_timing)
					appendStringInfoChar(fc_es->str, ',');
			}
			if (fc_has_temp_timing)
			{
				appendStringInfoString(fc_es->str, " temp");
				if (!INSTR_TIME_IS_ZERO(fc_usage->temp_blk_read_time))
					appendStringInfo(fc_es->str, " read=%0.3f",
									 INSTR_TIME_GET_MILLISEC(fc_usage->temp_blk_read_time));
				if (!INSTR_TIME_IS_ZERO(fc_usage->temp_blk_write_time))
					appendStringInfo(fc_es->str, " write=%0.3f",
									 INSTR_TIME_GET_MILLISEC(fc_usage->temp_blk_write_time));
			}
			appendStringInfoChar(fc_es->str, '\n');
		}

		if (fc_show_planning)
			fc_es->indent--;
	}
	else
	{
		ExplainPropertyInteger("Shared Hit Blocks", NULL,
							   fc_usage->shared_blks_hit, fc_es);
		ExplainPropertyInteger("Shared Read Blocks", NULL,
							   fc_usage->shared_blks_read, fc_es);
		ExplainPropertyInteger("Shared Dirtied Blocks", NULL,
							   fc_usage->shared_blks_dirtied, fc_es);
		ExplainPropertyInteger("Shared Written Blocks", NULL,
							   fc_usage->shared_blks_written, fc_es);
		ExplainPropertyInteger("Local Hit Blocks", NULL,
							   fc_usage->local_blks_hit, fc_es);
		ExplainPropertyInteger("Local Read Blocks", NULL,
							   fc_usage->local_blks_read, fc_es);
		ExplainPropertyInteger("Local Dirtied Blocks", NULL,
							   fc_usage->local_blks_dirtied, fc_es);
		ExplainPropertyInteger("Local Written Blocks", NULL,
							   fc_usage->local_blks_written, fc_es);
		ExplainPropertyInteger("Temp Read Blocks", NULL,
							   fc_usage->temp_blks_read, fc_es);
		ExplainPropertyInteger("Temp Written Blocks", NULL,
							   fc_usage->temp_blks_written, fc_es);
		if (track_io_timing)
		{
			ExplainPropertyFloat("I/O Read Time", "ms",
								 INSTR_TIME_GET_MILLISEC(fc_usage->blk_read_time),
								 3, fc_es);
			ExplainPropertyFloat("I/O Write Time", "ms",
								 INSTR_TIME_GET_MILLISEC(fc_usage->blk_write_time),
								 3, fc_es);
			ExplainPropertyFloat("Temp I/O Read Time", "ms",
								 INSTR_TIME_GET_MILLISEC(fc_usage->temp_blk_read_time),
								 3, fc_es);
			ExplainPropertyFloat("Temp I/O Write Time", "ms",
								 INSTR_TIME_GET_MILLISEC(fc_usage->temp_blk_write_time),
								 3, fc_es);
		}
	}
}

/*
 * 显示 WAL 使用情况的详细信息。
 */
static void fc_show_wal_usage(ExplainState *fc_es, const WalUsage *fc_usage)
{
	if (fc_es->format == EXPLAIN_FORMAT_TEXT)
	{
		/* 仅显示正计数值。 */
		if ((fc_usage->wal_records > 0) || (fc_usage->wal_fpi > 0) ||
			(fc_usage->wal_bytes > 0))
		{
			fc_ExplainIndentText(fc_es);
			appendStringInfoString(fc_es->str, "WAL:");

			if (fc_usage->wal_records > 0)
				appendStringInfo(fc_es->str, " records=%lld",
								 (long long) fc_usage->wal_records);
			if (fc_usage->wal_fpi > 0)
				appendStringInfo(fc_es->str, " fpi=%lld",
								 (long long) fc_usage->wal_fpi);
			if (fc_usage->wal_bytes > 0)
				appendStringInfo(fc_es->str, " bytes=" UINT64_FORMAT,
								 fc_usage->wal_bytes);
			appendStringInfoChar(fc_es->str, '\n');
		}
	}
	else
	{
		ExplainPropertyInteger("WAL Records", NULL,
							   fc_usage->wal_records, fc_es);
		ExplainPropertyInteger("WAL FPI", NULL,
							   fc_usage->wal_fpi, fc_es);
		ExplainPropertyUInteger("WAL Bytes", NULL,
								fc_usage->wal_bytes, fc_es);
	}
}

/*
 * 添加有关 IndexScan 或 IndexOnlyScan 的一些额外详细信息
 */
static void fc_ExplainIndexScanDetails(Oid fc_indexid, ScanDirection fc_indexorderdir,
						ExplainState *fc_es)
{
	const char *fc_indexname = fc_explain_get_index_name(fc_indexid);

	if (fc_es->format == EXPLAIN_FORMAT_TEXT)
	{
		if (ScanDirectionIsBackward(fc_indexorderdir))
			appendStringInfoString(fc_es->str, " Backward");
		appendStringInfo(fc_es->str, " using %s", quote_identifier(fc_indexname));
	}
	else
	{
		const char *fc_scandir;

		switch (fc_indexorderdir)
		{
			case BackwardScanDirection:
				fc_scandir = "Backward";
				break;
			case NoMovementScanDirection:
				fc_scandir = "NoMovement";
				break;
			case ForwardScanDirection:
				fc_scandir = "Forward";
				break;
			default:
				fc_scandir = "???";
				break;
		}
		ExplainPropertyText("Scan Direction", fc_scandir, fc_es);
		ExplainPropertyText("Index Name", fc_indexname, fc_es);
	}
}

/*
 * 显示 Scan 节点的目标
 */
static void fc_ExplainScanTarget(Scan *fc_plan, ExplainState *fc_es)
{
	fc_ExplainTargetRel((Plan *) fc_plan, fc_plan->scanrelid, fc_es);
}

/*
 * 显示 ModifyTable 节点的目标
 *
 * 在这里我们显示名义目标（即，最初查询中命名的关系）。如果实际目标不同，我们将在 show_modifytable_info() 中显示它们。
 */
static void fc_ExplainModifyTarget(ModifyTable *fc_plan, ExplainState *fc_es)
{
	fc_ExplainTargetRel((Plan *) fc_plan, fc_plan->nominalRelation, fc_es);
}

/*
 * 显示扫描或修改节点的目标关系
 */
static void fc_ExplainTargetRel(Plan *fc_plan, Index fc_rti, ExplainState *fc_es)
{
	char	   *fc_objectname = NULL;
	char	   *fc_namespace = NULL;
	const char *fc_objecttag = NULL;
	RangeTblEntry *fc_rte;
	char	   *fc_refname;

	fc_rte = rt_fetch(fc_rti, fc_es->rtable);
	fc_refname = (char *) list_nth(fc_es->rtable_names, fc_rti - 1);
	if (fc_refname == NULL)
		fc_refname = fc_rte->eref->aliasname;

	switch (nodeTag(fc_plan))
	{
		case T_SeqScan:
		case T_SampleScan:
		case T_IndexScan:
		case T_IndexOnlyScan:
		case T_BitmapHeapScan:
		case T_TidScan:
		case T_TidRangeScan:
		case T_ForeignScan:
		case T_CustomScan:
		case T_ModifyTable:
			/* 确保它在真实关系上 */
			Assert(fc_rte->rtekind == RTE_RELATION);
			fc_objectname = get_rel_name(fc_rte->relid);
			if (fc_es->verbose)
				fc_namespace = get_namespace_name_or_temp(get_rel_namespace(fc_rte->relid));
			fc_objecttag = "Relation Name";
			break;
		case T_FunctionScan:
			{
				FunctionScan *fc_fscan = (FunctionScan *) fc_plan;

				/* 确保它在 RangeFunction 上 */
				Assert(fc_rte->rtekind == RTE_FUNCTION);

				/*
				 * 如果表达式仍然是单一函数的函数调用，我们可以获取该函数的真实名称。
				 * 否则，放弃。（即使最初是单一函数调用，优化器也可能已将其简化。）
				 */
				if (list_length(fc_fscan->functions) == 1)
				{
					RangeTblFunction *fc_rtfunc = (RangeTblFunction *) linitial(fc_fscan->functions);

					if (IsA(fc_rtfunc->funcexpr, FuncExpr))
					{
						FuncExpr   *fc_funcexpr = (FuncExpr *) fc_rtfunc->funcexpr;
						Oid			fc_funcid = fc_funcexpr->funcid;

						fc_objectname = get_func_name(fc_funcid);
						if (fc_es->verbose)
							fc_namespace = get_namespace_name_or_temp(get_func_namespace(fc_funcid));
					}
				}
				fc_objecttag = "Function Name";
			}
			break;
		case T_TableFuncScan:
			Assert(fc_rte->rtekind == RTE_TABLEFUNC);
			fc_objectname = "xmltable";
			fc_objecttag = "Table Function Name";
			break;
		case T_ValuesScan:
			Assert(fc_rte->rtekind == RTE_VALUES);
			break;
		case T_CteScan:
			/* 确保它在非自引用 CTE 上 */
			Assert(fc_rte->rtekind == RTE_CTE);
			Assert(!fc_rte->self_reference);
			fc_objectname = fc_rte->ctename;
			fc_objecttag = "CTE Name";
			break;
		case T_NamedTuplestoreScan:
			Assert(fc_rte->rtekind == RTE_NAMEDTUPLESTORE);
			fc_objectname = fc_rte->enrname;
			fc_objecttag = "Tuplestore Name";
			break;
		case T_WorkTableScan:
			/* 确保它在自引用 CTE 上 */
			Assert(fc_rte->rtekind == RTE_CTE);
			Assert(fc_rte->self_reference);
			fc_objectname = fc_rte->ctename;
			fc_objecttag = "CTE Name";
			break;
		default:
			break;
	}

	if (fc_es->format == EXPLAIN_FORMAT_TEXT)
	{
		appendStringInfoString(fc_es->str, " on");
		if (fc_namespace != NULL)
			appendStringInfo(fc_es->str, " %s.%s", quote_identifier(fc_namespace),
							 quote_identifier(fc_objectname));
		else if (fc_objectname != NULL)
			appendStringInfo(fc_es->str, " %s", quote_identifier(fc_objectname));
		if (fc_objectname == NULL || strcmp(fc_refname, fc_objectname) != 0)
			appendStringInfo(fc_es->str, " %s", quote_identifier(fc_refname));
	}
	else
	{
		if (fc_objecttag != NULL && fc_objectname != NULL)
			ExplainPropertyText(fc_objecttag, fc_objectname, fc_es);
		if (fc_namespace != NULL)
			ExplainPropertyText("Schema", fc_namespace, fc_es);
		ExplainPropertyText("Alias", fc_refname, fc_es);
	}
}

/*
 * 显示 ModifyTable 节点的额外信息
 *
 * 我们在这里有三个目标。首先，如果有多个目标表或与名义目标不同，则识别实际目标。第二，给 FDW 一个机会显示有关外部目标的额外信息。第三，显示关于 ON CONFLICT 的信息。
 */
static void fc_show_modifytable_info(ModifyTableState *fc_mtstate, List *fc_ancestors,
					  ExplainState *fc_es)
{
	ModifyTable *fc_node = (ModifyTable *) fc_mtstate->ps.plan;
	const char *fc_operation;
	const char *fc_foperation;
	bool		fc_labeltargets;
	int			fc_j;
	List	   *fc_idxNames = NIL;
	ListCell   *fc_lst;

	switch (fc_node->operation)
	{
		case CMD_INSERT:
			fc_operation = "Insert";
			fc_foperation = "Foreign Insert";
			break;
		case CMD_UPDATE:
			fc_operation = "Update";
			fc_foperation = "Foreign Update";
			break;
		case CMD_DELETE:
			fc_operation = "Delete";
			fc_foperation = "Foreign Delete";
			break;
		case CMD_MERGE:
			fc_operation = "Merge";
			/* XXX 目前不支持，但避免编译器噪声 */
			fc_foperation = "Foreign Merge";
			break;
		default:
			fc_operation = "???";
			fc_foperation = "Foreign ???";
			break;
	}

	/* 我们是否应该明确标记目标关系？ */
	fc_labeltargets = (fc_mtstate->mt_nrels > 1 ||
					(fc_mtstate->mt_nrels == 1 &&
					 fc_mtstate->resultRelInfo[0].ri_RangeTableIndex != fc_node->nominalRelation));

	if (fc_labeltargets)
		ExplainOpenGroup("Target Tables", "Target Tables", false, fc_es);

	for (fc_j = 0; fc_j < fc_mtstate->mt_nrels; fc_j++)
	{
		ResultRelInfo *fc_resultRelInfo = fc_mtstate->resultRelInfo + fc_j;
		FdwRoutine *fc_fdwroutine = fc_resultRelInfo->ri_FdwRoutine;

		if (fc_labeltargets)
		{
			/* 为此目标打开一个组 */
			ExplainOpenGroup("Target Table", NULL, true, fc_es);

			/*
			 * 在文本模式下，用操作类型修饰每个目标，以便
			 * ExplainTargetRel 的输出 " on foo" 读起来不错。
			 */
			if (fc_es->format == EXPLAIN_FORMAT_TEXT)
			{
				fc_ExplainIndentText(fc_es);
				appendStringInfoString(fc_es->str,
									   fc_fdwroutine ? fc_foperation : fc_operation);
			}

			/* 确定目标 */
			fc_ExplainTargetRel((Plan *) fc_node,
							 fc_resultRelInfo->ri_RangeTableIndex,
							 fc_es);

			if (fc_es->format == EXPLAIN_FORMAT_TEXT)
			{
				appendStringInfoChar(fc_es->str, '\n');
				fc_es->indent++;
			}
		}

		/* 如有需要，给 FDW 一个机会 */
		if (!fc_resultRelInfo->ri_usesFdwDirectModify &&
			fc_fdwroutine != NULL &&
			fc_fdwroutine->ExplainForeignModify != NULL)
		{
			List	   *fc_fdw_private = (List *) list_nth(fc_node->fdwPrivLists, fc_j);

			fc_fdwroutine->ExplainForeignModify(fc_mtstate,
											 fc_resultRelInfo,
											 fc_fdw_private,
											 fc_j,
											 fc_es);
		}

		if (fc_labeltargets)
		{
			/* 撤销我们在文本格式中添加的缩进 */
			if (fc_es->format == EXPLAIN_FORMAT_TEXT)
				fc_es->indent--;

			/* 关闭组 */
			ExplainCloseGroup("Target Table", NULL, true, fc_es);
		}
	}

	/* 收集 ON CONFLICT 仲裁索引的名称 */
	foreach(fc_lst, fc_node->arbiterIndexes)
	{
		char	   *fc_indexname = get_rel_name(lfirst_oid(fc_lst));

		fc_idxNames = lappend(fc_idxNames, fc_indexname);
	}

	if (fc_node->onConflictAction != ONCONFLICT_NONE)
	{
		ExplainPropertyText("Conflict Resolution",
							fc_node->onConflictAction == ONCONFLICT_NOTHING ?
							"NOTHING" : "UPDATE",
							fc_es);

		/*
		 * 在 DO NOTHING 变体隐式忽略所有冲突时，不显示仲裁索引
		 */
		if (fc_idxNames)
			ExplainPropertyList("Conflict Arbiter Indexes", fc_idxNames, fc_es);

		/* ON CONFLICT DO UPDATE WHERE qual 特别显示 */
		if (fc_node->onConflictWhere)
		{
			fc_show_upper_qual((List *) fc_node->onConflictWhere, "Conflict Filter",
							&fc_mtstate->ps, fc_ancestors, fc_es);
			fc_show_instrumentation_count("Rows Removed by Conflict Filter", 1, &fc_mtstate->ps, fc_es);
		}

		/* EXPLAIN ANALYZE 显示每个提议元组的实际结果 */
		if (fc_es->analyze && fc_mtstate->ps.instrument)
		{
			double		fc_total;
			double		fc_insert_path;
			double		fc_other_path;

			InstrEndLoop(outerPlanState(fc_mtstate)->instrument);

			/* 计数源行数 */
			fc_total = outerPlanState(fc_mtstate)->instrument->ntuples;
			fc_other_path = fc_mtstate->ps.instrument->ntuples2;
			fc_insert_path = fc_total - fc_other_path;

			ExplainPropertyFloat("Tuples Inserted", NULL,
								 fc_insert_path, 0, fc_es);
			ExplainPropertyFloat("Conflicting Tuples", NULL,
								 fc_other_path, 0, fc_es);
		}
	}
	else if (fc_node->operation == CMD_MERGE)
	{
		/* EXPLAIN ANALYZE 显示处理的元组 */
		if (fc_es->analyze && fc_mtstate->ps.instrument)
		{
			double		fc_total;
			double		fc_insert_path;
			double		fc_update_path;
			double		fc_delete_path;
			double		fc_skipped_path;

			InstrEndLoop(outerPlanState(fc_mtstate)->instrument);

			/* 计数源行数 */
			fc_total = outerPlanState(fc_mtstate)->instrument->ntuples;
			fc_insert_path = fc_mtstate->mt_merge_inserted;
			fc_update_path = fc_mtstate->mt_merge_updated;
			fc_delete_path = fc_mtstate->mt_merge_deleted;
			fc_skipped_path = fc_total - fc_insert_path - fc_update_path - fc_delete_path;
			Assert(fc_skipped_path >= 0);

			if (fc_es->format == EXPLAIN_FORMAT_TEXT)
			{
				if (fc_total > 0)
				{
					fc_ExplainIndentText(fc_es);
					appendStringInfoString(fc_es->str, "Tuples:");
					if (fc_insert_path > 0)
						appendStringInfo(fc_es->str, " inserted=%.0f", fc_insert_path);
					if (fc_update_path > 0)
						appendStringInfo(fc_es->str, " updated=%.0f", fc_update_path);
					if (fc_delete_path > 0)
						appendStringInfo(fc_es->str, " deleted=%.0f", fc_delete_path);
					if (fc_skipped_path > 0)
						appendStringInfo(fc_es->str, " skipped=%.0f", fc_skipped_path);
					appendStringInfoChar(fc_es->str, '\n');
				}
			}
			else
			{
				ExplainPropertyFloat("Tuples Inserted", NULL, fc_insert_path, 0, fc_es);
				ExplainPropertyFloat("Tuples Updated", NULL, fc_update_path, 0, fc_es);
				ExplainPropertyFloat("Tuples Deleted", NULL, fc_delete_path, 0, fc_es);
				ExplainPropertyFloat("Tuples Skipped", NULL, fc_skipped_path, 0, fc_es);
			}
		}
	}

	if (fc_labeltargets)
		ExplainCloseGroup("Target Tables", "Target Tables", false, fc_es);
}

/*
 * 解释 Append、MergeAppend、
 * BitmapAnd 或 BitmapOr 节点的组成计划。
 *
 * 祖先列表应已包含这些
 * 计划的直接父级。
 */
static void fc_ExplainMemberNodes(PlanState **fc_planstates, int fc_nplans,
				   List *fc_ancestors, ExplainState *fc_es)
{
	int			fc_j;

	for (fc_j = 0; fc_j < fc_nplans; fc_j++)
		fc_ExplainNode(fc_planstates[fc_j], fc_ancestors,
					"Member", NULL, fc_es);
}

/*
 * 报告任何已修剪的 Append 或 MergeAppend 节点的子节点。
 *
 * nplans 指示活动子计划的数量。
 * nchildren 指示 Plan 中原始子节点的数量；
 * 其中一些可能已被运行时修剪代码修剪。
 */
static void fc_ExplainMissingMembers(int fc_nplans, int fc_nchildren, ExplainState *fc_es)
{
	if (fc_nplans < fc_nchildren || fc_es->format != EXPLAIN_FORMAT_TEXT)
		ExplainPropertyInteger("Subplans Removed", NULL,
							   fc_nchildren - fc_nplans, fc_es);
}

/*
 * 解释 SubPlans（或 initPlans，亦使用 SubPlan 节点）的列表。
 *
 * 祖先列表应已包含这些
 * SubPlans 的直接父级。
 */
static void fc_ExplainSubPlans(List *fc_plans, List *fc_ancestors,
				const char *fc_relationship, ExplainState *fc_es)
{
	ListCell   *fc_lst;

	foreach(fc_lst, fc_plans)
	{
		SubPlanState *fc_sps = (SubPlanState *) lfirst(fc_lst);
		SubPlan    *fc_sp = fc_sps->subplan;

		/*
		 * 可以有多个 SubPlan 节点引用相同的物理
		 * 子计划（相同的 plan_id，即其在 PlannedStmt.subplans 中的索引）。
		 * 我们应只打印一次子计划，因此跟踪我们已经
		 * 打印过的子计划。这个状态必须在计划树中全局，因为
		 * 重复节点可能位于不同的计划节点，例如位图
		 * 索引扫描的 indexqual 及其父堆扫描的重新检查 qual。（在这种情况下，
		 * 我们并不太担心显示该子计划附加在哪个计划节点上。）
		 */
		if (bms_is_member(fc_sp->plan_id, fc_es->printed_subplans))
			continue;
		fc_es->printed_subplans = bms_add_member(fc_es->printed_subplans,
											  fc_sp->plan_id);

		/*
		 * 将 SubPlan 节点视为其中一个计划节点的祖先，
		 * 以便 ruleutils.c 能找到子计划参数的引用。
		 */
		fc_ancestors = lcons(fc_sp, fc_ancestors);

		fc_ExplainNode(fc_sps->planstate, fc_ancestors,
					fc_relationship, fc_sp->plan_name, fc_es);

		fc_ancestors = list_delete_first(fc_ancestors);
	}
}

/*
 * 解释 CustomScan 的子节点列表。
 */
static void fc_ExplainCustomChildren(CustomScanState *fc_css, List *fc_ancestors, ExplainState *fc_es)
{
	ListCell   *fc_cell;
	const char *fc_label =
	(list_length(fc_css->custom_ps) != 1 ? "children" : "child");

	foreach(fc_cell, fc_css->custom_ps)
		fc_ExplainNode((PlanState *) lfirst(fc_cell), fc_ancestors, fc_label, NULL, fc_es);
}

/*
 * 为每个计划节点创建一个工作区，用于收集每个工作线程的数据。
 *
 * 与每个工作线程相关的输出将暂时“搁置”到一个
 * 单独的缓冲区中，一旦我们处理完该计划节点的所有数据，
 * 就会将其合并到主输出流中。这使得为每个工作线程生成
 * 一组连贯的字段成为可能，即使生成这些字段的代码在
 * 本文件中的几个不同位置。这样的搁置字段组的格式由
 * ExplainOpenSetAsideGroup 和 ExplainSaveGroup/ExplainRestoreGroup 管理。
 */
static ExplainWorkersState *
fc_ExplainCreateWorkersState(int fc_num_workers)
{
	ExplainWorkersState *fc_wstate;

	fc_wstate = (ExplainWorkersState *) palloc(sizeof(ExplainWorkersState));
	fc_wstate->num_workers = fc_num_workers;
	fc_wstate->worker_inited = (bool *) palloc0(fc_num_workers * sizeof(bool));
	fc_wstate->worker_str = (StringInfoData *)
		palloc0(fc_num_workers * sizeof(StringInfoData));
	fc_wstate->worker_state_save = (int *) palloc(fc_num_workers * sizeof(int));
	return fc_wstate;
}

/*
 * 开始或恢复为工作线程 N 的搁置组输出。
 */
static void fc_ExplainOpenWorker(int fc_n, ExplainState *fc_es)
{
	ExplainWorkersState *fc_wstate = fc_es->workers_state;

	Assert(fc_wstate);
	Assert(fc_n >= 0 && fc_n < fc_wstate->num_workers);

	/* 保存之前的输出缓冲区指针 */
	fc_wstate->prev_str = fc_es->str;

	if (!fc_wstate->worker_inited[fc_n])
	{
		/* 第一次通过，因此为该工作线程创建缓冲区 */
		initStringInfo(&fc_wstate->worker_str[fc_n]);
		fc_es->str = &fc_wstate->worker_str[fc_n];

		/*
		 * 为该工作线程的字段
		 * 组推送合适的初始格式状态。我们允许一个额外的逻辑嵌套级别，
		 * 因为该组最终将包裹在外围“Workers”组中。
		 */
		fc_ExplainOpenSetAsideGroup("Worker", NULL, true, 2, fc_es);

		/*
		 * 在非 TEXT 格式中，即使该工作线程没有其他数据，
		 * 我们也始终发出一个“工作线程编号”字段。
		 */
		if (fc_es->format != EXPLAIN_FORMAT_TEXT)
			ExplainPropertyInteger("Worker Number", NULL, fc_n, fc_es);

		fc_wstate->worker_inited[fc_n] = true;
	}
	else
	{
		/* 恢复我们已经发出了一些数据的工作线程的输出 */
		fc_es->str = &fc_wstate->worker_str[fc_n];

		/* 恢复上一个 ExplainCloseWorker() 保存的格式状态 */
		fc_ExplainRestoreGroup(fc_es, 2, &fc_wstate->worker_state_save[fc_n]);
	}

	/*
	 * 在 TEXT 格式中，为该工作线程的第一行输出加上
	 * "Worker N:" 前缀。然后，任何额外的行应该比
	 * "Worker N" 行多缩进一度。
	 */
	if (fc_es->format == EXPLAIN_FORMAT_TEXT)
	{
		if (fc_es->str->len == 0)
		{
			fc_ExplainIndentText(fc_es);
			appendStringInfo(fc_es->str, "Worker %d:  ", fc_n);
		}

		fc_es->indent++;
	}
}

/*
 * 结束工作线程 N 的输出 --- 必须与之前的 ExplainOpenWorker 调用配对
 */
static void fc_ExplainCloseWorker(int fc_n, ExplainState *fc_es)
{
	ExplainWorkersState *fc_wstate = fc_es->workers_state;

	Assert(fc_wstate);
	Assert(fc_n >= 0 && fc_n < fc_wstate->num_workers);
	Assert(fc_wstate->worker_inited[fc_n]);

	/*
	 * 保存格式状态，以备我们进行另一个 ExplainOpenWorker()，
	 * 然后弹出格式栈。
	 */
	fc_ExplainSaveGroup(fc_es, 2, &fc_wstate->worker_state_save[fc_n]);

	/*
	 * 在 TEXT 格式中，如果我们实际上没有生成任何输出行，
	 * 则截断由 ExplainOpenWorker 发出的部分行。 (这是
	 * 为了避免虚假的输出，例如，show_buffer_usage 选择不为
	 * 工作线程打印任何内容。）还要修正缩进级别。
	 */
	if (fc_es->format == EXPLAIN_FORMAT_TEXT)
	{
		while (fc_es->str->len > 0 && fc_es->str->data[fc_es->str->len - 1] != '\n')
			fc_es->str->data[--(fc_es->str->len)] = '\0';

		fc_es->indent--;
	}

	/* 恢复先前的输出缓冲区指针 */
	fc_es->str = fc_wstate->prev_str;
}

/*
 * 打印当前节点的每个工作线程信息，然后释放 ExplainWorkersState。
 */
static void fc_ExplainFlushWorkersState(ExplainState *fc_es)
{
	ExplainWorkersState *fc_wstate = fc_es->workers_state;

	ExplainOpenGroup("Workers", "Workers", false, fc_es);
	for (int fc_i = 0; fc_i < fc_wstate->num_workers; fc_i++)
	{
		if (fc_wstate->worker_inited[fc_i])
		{
			/* 这必须与之前的 ExplainOpenSetAsideGroup 调用匹配 */
			ExplainOpenGroup("Worker", NULL, true, fc_es);
			appendStringInfoString(fc_es->str, fc_wstate->worker_str[fc_i].data);
			ExplainCloseGroup("Worker", NULL, true, fc_es);

			pfree(fc_wstate->worker_str[fc_i].data);
		}
	}
	ExplainCloseGroup("Workers", "Workers", false, fc_es);

	pfree(fc_wstate->worker_inited);
	pfree(fc_wstate->worker_str);
	pfree(fc_wstate->worker_state_save);
	pfree(fc_wstate);
}

/*
 * 解释一个属性，例如排序键或目标，其形式为
 * 未标记项的列表。 "data" 是一个 C 字符串的列表。
 */
void ExplainPropertyList(const char *fc_qlabel, List *fc_data, ExplainState *fc_es)
{
	ListCell   *fc_lc;
	bool		fc_first = true;

	switch (fc_es->format)
	{
		case EXPLAIN_FORMAT_TEXT:
			fc_ExplainIndentText(fc_es);
			appendStringInfo(fc_es->str, "%s: ", fc_qlabel);
			foreach(fc_lc, fc_data)
			{
				if (!fc_first)
					appendStringInfoString(fc_es->str, ", ");
				appendStringInfoString(fc_es->str, (const char *) lfirst(fc_lc));
				fc_first = false;
			}
			appendStringInfoChar(fc_es->str, '\n');
			break;

		case EXPLAIN_FORMAT_XML:
			fc_ExplainXMLTag(fc_qlabel, X_OPENING, fc_es);
			foreach(fc_lc, fc_data)
			{
				char	   *fc_str;

				appendStringInfoSpaces(fc_es->str, fc_es->indent * 2 + 2);
				appendStringInfoString(fc_es->str, "<Item>");
				fc_str = escape_xml((const char *) lfirst(fc_lc));
				appendStringInfoString(fc_es->str, fc_str);
				pfree(fc_str);
				appendStringInfoString(fc_es->str, "</Item>\n");
			}
			fc_ExplainXMLTag(fc_qlabel, X_CLOSING, fc_es);
			break;

		case EXPLAIN_FORMAT_JSON:
			fc_ExplainJSONLineEnding(fc_es);
			appendStringInfoSpaces(fc_es->str, fc_es->indent * 2);
			escape_json(fc_es->str, fc_qlabel);
			appendStringInfoString(fc_es->str, ": [");
			foreach(fc_lc, fc_data)
			{
				if (!fc_first)
					appendStringInfoString(fc_es->str, ", ");
				escape_json(fc_es->str, (const char *) lfirst(fc_lc));
				fc_first = false;
			}
			appendStringInfoChar(fc_es->str, ']');
			break;

		case EXPLAIN_FORMAT_YAML:
			fc_ExplainYAMLLineStarting(fc_es);
			appendStringInfo(fc_es->str, "%s: ", fc_qlabel);
			foreach(fc_lc, fc_data)
			{
				appendStringInfoChar(fc_es->str, '\n');
				appendStringInfoSpaces(fc_es->str, fc_es->indent * 2 + 2);
				appendStringInfoString(fc_es->str, "- ");
				fc_escape_yaml(fc_es->str, (const char *) lfirst(fc_lc));
			}
			break;
	}
}

/*
 * 解释一个属性，其形式为另一个列表中的未标记项列表。
 * "data" 是一个 C 字符串的列表。
 */
void ExplainPropertyListNested(const char *fc_qlabel, List *fc_data, ExplainState *fc_es)
{
	ListCell   *fc_lc;
	bool		fc_first = true;

	switch (fc_es->format)
	{
		case EXPLAIN_FORMAT_TEXT:
		case EXPLAIN_FORMAT_XML:
			ExplainPropertyList(fc_qlabel, fc_data, fc_es);
			return;

		case EXPLAIN_FORMAT_JSON:
			fc_ExplainJSONLineEnding(fc_es);
			appendStringInfoSpaces(fc_es->str, fc_es->indent * 2);
			appendStringInfoChar(fc_es->str, '[');
			foreach(fc_lc, fc_data)
			{
				if (!fc_first)
					appendStringInfoString(fc_es->str, ", ");
				escape_json(fc_es->str, (const char *) lfirst(fc_lc));
				fc_first = false;
			}
			appendStringInfoChar(fc_es->str, ']');
			break;

		case EXPLAIN_FORMAT_YAML:
			fc_ExplainYAMLLineStarting(fc_es);
			appendStringInfoString(fc_es->str, "- [");
			foreach(fc_lc, fc_data)
			{
				if (!fc_first)
					appendStringInfoString(fc_es->str, ", ");
				fc_escape_yaml(fc_es->str, (const char *) lfirst(fc_lc));
				fc_first = false;
			}
			appendStringInfoChar(fc_es->str, ']');
			break;
	}
}

/*
 * 解释一个简单属性。
 *
 * 如果 "numeric" 为真，则值是一个数字（或在 JSON 中
 * 不需要引号的其他值）。
 *
 * 如果 unit 非 NULL，则文本格式将在值之后显示它。
 *
 * 通常不应直接调用，而应通过某个数据类型
 * 特定的例程 ExplainPropertyText、ExplainPropertyInteger 等。
 */
static void fc_ExplainProperty(const char *fc_qlabel, const char *fc_unit, const char *fc_value,
				bool fc_numeric, ExplainState *fc_es)
{
	switch (fc_es->format)
	{
		case EXPLAIN_FORMAT_TEXT:
			fc_ExplainIndentText(fc_es);
			if (fc_unit)
				appendStringInfo(fc_es->str, "%s: %s %s\n", fc_qlabel, fc_value, fc_unit);
			else
				appendStringInfo(fc_es->str, "%s: %s\n", fc_qlabel, fc_value);
			break;

		case EXPLAIN_FORMAT_XML:
			{
				char	   *fc_str;

				appendStringInfoSpaces(fc_es->str, fc_es->indent * 2);
				fc_ExplainXMLTag(fc_qlabel, X_OPENING | X_NOWHITESPACE, fc_es);
				fc_str = escape_xml(fc_value);
				appendStringInfoString(fc_es->str, fc_str);
				pfree(fc_str);
				fc_ExplainXMLTag(fc_qlabel, X_CLOSING | X_NOWHITESPACE, fc_es);
				appendStringInfoChar(fc_es->str, '\n');
			}
			break;

		case EXPLAIN_FORMAT_JSON:
			fc_ExplainJSONLineEnding(fc_es);
			appendStringInfoSpaces(fc_es->str, fc_es->indent * 2);
			escape_json(fc_es->str, fc_qlabel);
			appendStringInfoString(fc_es->str, ": ");
			if (fc_numeric)
				appendStringInfoString(fc_es->str, fc_value);
			else
				escape_json(fc_es->str, fc_value);
			break;

		case EXPLAIN_FORMAT_YAML:
			fc_ExplainYAMLLineStarting(fc_es);
			appendStringInfo(fc_es->str, "%s: ", fc_qlabel);
			if (fc_numeric)
				appendStringInfoString(fc_es->str, fc_value);
			else
				fc_escape_yaml(fc_es->str, fc_value);
			break;
	}
}

/*
 * 解释一个字符串值属性。
 */
void ExplainPropertyText(const char *fc_qlabel, const char *fc_value, ExplainState *fc_es)
{
	fc_ExplainProperty(fc_qlabel, NULL, fc_value, false, fc_es);
}

/*
 * 解释一个整数值属性。
 */
void ExplainPropertyInteger(const char *fc_qlabel, const char *fc_unit, int64 fc_value,
					   ExplainState *fc_es)
{
	char		fc_buf[32];

	snprintf(fc_buf, sizeof(fc_buf), INT64_FORMAT, fc_value);
	fc_ExplainProperty(fc_qlabel, fc_unit, fc_buf, true, fc_es);
}

/*
 * 解释一个无符号整数值属性。
 */
void ExplainPropertyUInteger(const char *fc_qlabel, const char *fc_unit, uint64 fc_value,
						ExplainState *fc_es)
{
	char		fc_buf[32];

	snprintf(fc_buf, sizeof(fc_buf), UINT64_FORMAT, fc_value);
	fc_ExplainProperty(fc_qlabel, fc_unit, fc_buf, true, fc_es);
}

/*
 * 解释一个浮点值属性，使用指定的
 * 小数位数。
 */
void ExplainPropertyFloat(const char *fc_qlabel, const char *fc_unit, double fc_value,
					 int fc_ndigits, ExplainState *fc_es)
{
	char	   *fc_buf;

	fc_buf = psprintf("%.*f", fc_ndigits, fc_value);
	fc_ExplainProperty(fc_qlabel, fc_unit, fc_buf, true, fc_es);
	pfree(fc_buf);
}

/*
 * 解释一个布尔值属性。
 */
void ExplainPropertyBool(const char *fc_qlabel, bool fc_value, ExplainState *fc_es)
{
	fc_ExplainProperty(fc_qlabel, NULL, fc_value ? "true" : "false", true, fc_es);
}

/*
 * 打开一组相关对象。
 *
 * objtype 是组对象的类型，labelname 是它在
 * 包含对象中的标签（如果有的话）。
 *
 * 如果 labeled 为真，则组成员将被标记为属性，
 * 而如果为假，则它们将是未标记对象。
 */
void ExplainOpenGroup(const char *fc_objtype, const char *fc_labelname,
				 bool fc_labeled, ExplainState *fc_es)
{
	switch (fc_es->format)
	{
		case EXPLAIN_FORMAT_TEXT:
			/* 无需操作 */
			break;

		case EXPLAIN_FORMAT_XML:
			fc_ExplainXMLTag(fc_objtype, X_OPENING, fc_es);
			fc_es->indent++;
			break;

		case EXPLAIN_FORMAT_JSON:
			fc_ExplainJSONLineEnding(fc_es);
			appendStringInfoSpaces(fc_es->str, 2 * fc_es->indent);
			if (fc_labelname)
			{
				escape_json(fc_es->str, fc_labelname);
				appendStringInfoString(fc_es->str, ": ");
			}
			appendStringInfoChar(fc_es->str, fc_labeled ? '{' : '[');

			/*
			 * 在 JSON 格式中，grouping_stack 是一个整数列表。0 表示
			 * 我们在此分组级别未发出任何内容，1 表示我们
			 * 发出了某些内容（因此下一个项目需要一个逗号）。参见
			 * ExplainJSONLineEnding()。
			 */
			fc_es->grouping_stack = lcons_int(0, fc_es->grouping_stack);
			fc_es->indent++;
			break;

		case EXPLAIN_FORMAT_YAML:

			/*
			 * 在 YAML 格式中，分组栈是一个整数列表。0 表示
			 * 我们在此分组级别未发出任何内容，并且此分组
			 * 级别是未标记的，必须用 "- " 标记。参见
			 * ExplainYAMLLineStarting()。
			 */
			fc_ExplainYAMLLineStarting(fc_es);
			if (fc_labelname)
			{
				appendStringInfo(fc_es->str, "%s: ", fc_labelname);
				fc_es->grouping_stack = lcons_int(1, fc_es->grouping_stack);
			}
			else
			{
				appendStringInfoString(fc_es->str, "- ");
				fc_es->grouping_stack = lcons_int(0, fc_es->grouping_stack);
			}
			fc_es->indent++;
			break;
	}
}

/*
 * 关闭一组相关对象。
 * 参数必须与相应的 ExplainOpenGroup 调用匹配。
 */
void ExplainCloseGroup(const char *fc_objtype, const char *fc_labelname,
				  bool fc_labeled, ExplainState *fc_es)
{
	switch (fc_es->format)
	{
		case EXPLAIN_FORMAT_TEXT:
			/* 无需操作 */
			break;

		case EXPLAIN_FORMAT_XML:
			fc_es->indent--;
			fc_ExplainXMLTag(fc_objtype, X_CLOSING, fc_es);
			break;

		case EXPLAIN_FORMAT_JSON:
			fc_es->indent--;
			appendStringInfoChar(fc_es->str, '\n');
			appendStringInfoSpaces(fc_es->str, 2 * fc_es->indent);
			appendStringInfoChar(fc_es->str, fc_labeled ? '}' : ']');
			fc_es->grouping_stack = list_delete_first(fc_es->grouping_stack);
			break;

		case EXPLAIN_FORMAT_YAML:
			fc_es->indent--;
			fc_es->grouping_stack = list_delete_first(fc_es->grouping_stack);
			break;
	}
}

/*
 * 打开一组相关对象，而不发出实际数据。
 *
 * 准备格式状态，就好像我们正在开始一个具有
 * 所识别属性的组，但实际上不发出任何内容。此调用之后的输出
 * 可以重定向到一个单独的输出缓冲区，
 * 然后在执行常规的 ExplainOpenGroup 调用后（使用相同的参数）
 * 最终附加到主输出缓冲区。
 *
 * 额外的“depth”参数是新组相对于当前的深度。
 * 它可能大于 1，以防最终输出会被封闭在
 * 更多嵌套组级别中。我们假设在准备该组的输出时
 * 不需要跟踪这些级别的格式状态。
 *
 * 没有 ExplainCloseSetAsideGroup --- 在当前用法中，我们始终
 * 使用 ExplainSaveGroup 弹出此状态。
 */
static void fc_ExplainOpenSetAsideGroup(const char *fc_objtype, const char *fc_labelname,
						 bool fc_labeled, int fc_depth, ExplainState *fc_es)
{
	switch (fc_es->format)
	{
		case EXPLAIN_FORMAT_TEXT:
			/* 无需操作 */
			break;

		case EXPLAIN_FORMAT_XML:
			fc_es->indent += fc_depth;
			break;

		case EXPLAIN_FORMAT_JSON:
			fc_es->grouping_stack = lcons_int(0, fc_es->grouping_stack);
			fc_es->indent += fc_depth;
			break;

		case EXPLAIN_FORMAT_YAML:
			if (fc_labelname)
				fc_es->grouping_stack = lcons_int(1, fc_es->grouping_stack);
			else
				fc_es->grouping_stack = lcons_int(0, fc_es->grouping_stack);
			fc_es->indent += fc_depth;
			break;
	}
}

/*
 * 弹出一层分组状态，允许稍后重新推送。
 *
 * 这通常在 ExplainOpenSetAsideGroup 之后使用；传递
 * 用于该操作的相同“depth”。
 *
 * 这不应发出任何输出。如果需要保存状态，
 * 将其保存到 *state_save。当前，一个整数保存区域对
 * 所有格式都足够，但我们可能需要在某一天重新审视这个问题。
 */
static void fc_ExplainSaveGroup(ExplainState *fc_es, int fc_depth, int *fc_state_save)
{
	switch (fc_es->format)
	{
		case EXPLAIN_FORMAT_TEXT:
			/* 无需操作 */
			break;

		case EXPLAIN_FORMAT_XML:
			fc_es->indent -= fc_depth;
			break;

		case EXPLAIN_FORMAT_JSON:
			fc_es->indent -= fc_depth;
			*fc_state_save = linitial_int(fc_es->grouping_stack);
			fc_es->grouping_stack = list_delete_first(fc_es->grouping_stack);
			break;

		case EXPLAIN_FORMAT_YAML:
			fc_es->indent -= fc_depth;
			*fc_state_save = linitial_int(fc_es->grouping_stack);
			fc_es->grouping_stack = list_delete_first(fc_es->grouping_stack);
			break;
	}
}

/*
 * 重新推送一层分组状态，撤消 ExplainSaveGroup 的影响。
 */
static void fc_ExplainRestoreGroup(ExplainState *fc_es, int fc_depth, int *fc_state_save)
{
	switch (fc_es->format)
	{
		case EXPLAIN_FORMAT_TEXT:
			/* 无需操作 */
			break;

		case EXPLAIN_FORMAT_XML:
			fc_es->indent += fc_depth;
			break;

		case EXPLAIN_FORMAT_JSON:
			fc_es->grouping_stack = lcons_int(*fc_state_save, fc_es->grouping_stack);
			fc_es->indent += fc_depth;
			break;

		case EXPLAIN_FORMAT_YAML:
			fc_es->grouping_stack = lcons_int(*fc_state_save, fc_es->grouping_stack);
			fc_es->indent += fc_depth;
			break;
	}
}

/*
 * 发出一个“虚拟”的组，该组永远没有任何成员。
 *
 * objtype 是组对象的类型，labelname 是其在
 * 包含对象中的标签（如果有）。
 */
static void fc_ExplainDummyGroup(const char *fc_objtype, const char *fc_labelname, ExplainState *fc_es)
{
	switch (fc_es->format)
	{
		case EXPLAIN_FORMAT_TEXT:
			/* 无需操作 */
			break;

		case EXPLAIN_FORMAT_XML:
			fc_ExplainXMLTag(fc_objtype, X_CLOSE_IMMEDIATE, fc_es);
			break;

		case EXPLAIN_FORMAT_JSON:
			fc_ExplainJSONLineEnding(fc_es);
			appendStringInfoSpaces(fc_es->str, 2 * fc_es->indent);
			if (fc_labelname)
			{
				escape_json(fc_es->str, fc_labelname);
				appendStringInfoString(fc_es->str, ": ");
			}
			escape_json(fc_es->str, fc_objtype);
			break;

		case EXPLAIN_FORMAT_YAML:
			fc_ExplainYAMLLineStarting(fc_es);
			if (fc_labelname)
			{
				fc_escape_yaml(fc_es->str, fc_labelname);
				appendStringInfoString(fc_es->str, ": ");
			}
			else
			{
				appendStringInfoString(fc_es->str, "- ");
			}
			fc_escape_yaml(fc_es->str, fc_objtype);
			break;
	}
}

/*
 * 发出输出开始的样板。
 *
 * 这与处理子组有足够的不同，因此我们需要
 * 一对单独的子程序。
 */
void ExplainBeginOutput(ExplainState *fc_es)
{
	switch (fc_es->format)
	{
		case EXPLAIN_FORMAT_TEXT:
			/* 无需操作 */
			break;

		case EXPLAIN_FORMAT_XML:
			appendStringInfoString(fc_es->str,
								   "<explain xmlns=\"http://www.postgresql.org/2009/explain\">\n");
			fc_es->indent++;
			break;

		case EXPLAIN_FORMAT_JSON:
			/* 顶层结构是一个计划数组 */
			appendStringInfoChar(fc_es->str, '[');
			fc_es->grouping_stack = lcons_int(0, fc_es->grouping_stack);
			fc_es->indent++;
			break;

		case EXPLAIN_FORMAT_YAML:
			fc_es->grouping_stack = lcons_int(0, fc_es->grouping_stack);
			break;
	}
}

/*
 * 发出输出结束的样板。
 */
void ExplainEndOutput(ExplainState *fc_es)
{
	switch (fc_es->format)
	{
		case EXPLAIN_FORMAT_TEXT:
			/* 无需操作 */
			break;

		case EXPLAIN_FORMAT_XML:
			fc_es->indent--;
			appendStringInfoString(fc_es->str, "</explain>");
			break;

		case EXPLAIN_FORMAT_JSON:
			fc_es->indent--;
			appendStringInfoString(fc_es->str, "\n]");
			fc_es->grouping_stack = list_delete_first(fc_es->grouping_stack);
			break;

		case EXPLAIN_FORMAT_YAML:
			fc_es->grouping_stack = list_delete_first(fc_es->grouping_stack);
			break;
	}
}

/*
 * 在多个计划之间放置适当的分隔符。
 */
void ExplainSeparatePlans(ExplainState *fc_es)
{
	switch (fc_es->format)
	{
		case EXPLAIN_FORMAT_TEXT:
			/* 添加空行 */
			appendStringInfoChar(fc_es->str, '\n');
			break;

		case EXPLAIN_FORMAT_XML:
		case EXPLAIN_FORMAT_JSON:
		case EXPLAIN_FORMAT_YAML:
			/* 无需操作 */
			break;
	}
}

/*
 * 发出开启或关闭的 XML 标签。
 *
 * “flags”必须包含 X_OPENING、X_CLOSING 或 X_CLOSE_IMMEDIATE。
 * 可选择地，将 X_NOWHITESPACE 或运算以抑制
 * 我们通常会添加的空白。
 *
 * XML 对标签名称的限制比我们其他输出格式更严格，例如它们不能
 * 包含空格或斜杠。用破折号替换无效字符，
 * 例如将“I/O Read Time”替换为“I-O-Read-Time”。
 */
static void fc_ExplainXMLTag(const char *fc_tagname, int fc_flags, ExplainState *fc_es)
{
	const char *fc_s;
	const char *fc_valid = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.";

	if ((fc_flags & X_NOWHITESPACE) == 0)
		appendStringInfoSpaces(fc_es->str, 2 * fc_es->indent);
	appendStringInfoCharMacro(fc_es->str, '<');
	if ((fc_flags & X_CLOSING) != 0)
		appendStringInfoCharMacro(fc_es->str, '/');
	for (fc_s = fc_tagname; *fc_s; fc_s++)
		appendStringInfoChar(fc_es->str, strchr(fc_valid, *fc_s) ? *fc_s : '-');
	if ((fc_flags & X_CLOSE_IMMEDIATE) != 0)
		appendStringInfoString(fc_es->str, " /");
	appendStringInfoCharMacro(fc_es->str, '>');
	if ((fc_flags & X_NOWHITESPACE) == 0)
		appendStringInfoCharMacro(fc_es->str, '\n');
}

/*
 * 缩进文本格式行。
 *
 * 我们每个缩进级别缩进两个空格。然而，当为
 * 并行工作者发出数据时，当前行上可能已经有数据
 * （参见 ExplainOpenWorker）；在这种情况下，不要再缩进。
 */
static void fc_ExplainIndentText(ExplainState *fc_es)
{
	Assert(fc_es->format == EXPLAIN_FORMAT_TEXT);
	if (fc_es->str->len == 0 || fc_es->str->data[fc_es->str->len - 1] == '\n')
		appendStringInfoSpaces(fc_es->str, fc_es->indent * 2);
}

/*
 * 发出 JSON 行结束符。
 *
 * JSON 要求每个属性后面都有一个逗号，除了最后一个。为了便利，
 * 在 JSON 格式中，每个属性发出的文本在前一行换行
 * 之前的地方开始（如果适用则在逗号之前）。
 */
static void fc_ExplainJSONLineEnding(ExplainState *fc_es)
{
	Assert(fc_es->format == EXPLAIN_FORMAT_JSON);
	if (linitial_int(fc_es->grouping_stack) != 0)
		appendStringInfoChar(fc_es->str, ',');
	else
		linitial_int(fc_es->grouping_stack) = 1;
	appendStringInfoChar(fc_es->str, '\n');
}

/*
 * 缩进一个 YAML 行。
 *
 * YAML 行通常每个缩进级别缩进两个空格。
 * 每个属性发出的文本在前一行的换行符之前开始，
 * 除了无标签组中的第一个属性，它在介绍该组的 "- " 之后立即开始。
 * 组的第一个属性与开头的 "- " 在同一行。
 */
static void fc_ExplainYAMLLineStarting(ExplainState *fc_es)
{
	Assert(fc_es->format == EXPLAIN_FORMAT_YAML);
	if (linitial_int(fc_es->grouping_stack) == 0)
	{
		linitial_int(fc_es->grouping_stack) = 1;
	}
	else
	{
		appendStringInfoChar(fc_es->str, '\n');
		appendStringInfoSpaces(fc_es->str, fc_es->indent * 2);
	}
}

/*
 * YAML 是 JSON 的超集；不幸的是，YAML 的引号规则
 * 非常复杂 -- 如 http://yaml.org/spec/1.2/spec.html 的
 * 5.3 和 7.3.3 节所述 -- 所以我们选择将所有内容都加引号。
 * 空字符串、前后有空格的字符串以及包含各种特殊字符的字符串
 * 必须加引号，否则输出是无效的；而其他看似无害的字符串
 * 如 "0xa" 或 "true" 必须加引号，以免它们被解释为
 * 十六进制或布尔常量，而不是字符串。
 */
static void fc_escape_yaml(StringInfo fc_buf, const char *fc_str)
{
	escape_json(fc_buf, fc_str);
}
